2018-05-31 03:35:51 +03:00
|
|
|
const sinon = require("sinon");
|
2018-05-04 20:36:06 +03:00
|
|
|
const { expect } = require("chai");
|
2018-05-04 23:01:45 +03:00
|
|
|
const Hawk = require("hawk");
|
2018-05-04 20:36:06 +03:00
|
|
|
|
2018-05-09 21:27:08 +03:00
|
|
|
const { DEV_CREDENTIALS, DEFAULT_HAWK_ALGORITHM } = require("../lib/constants");
|
2018-05-04 23:01:45 +03:00
|
|
|
|
2018-05-09 21:27:08 +03:00
|
|
|
const {
|
|
|
|
mocks,
|
|
|
|
makePromiseFn,
|
2018-05-24 23:30:18 +03:00
|
|
|
env: { UPSTREAM_SERVICE_URL, CREDENTIALS_TABLE, QUEUE_NAME, CONTENT_BUCKET },
|
2018-05-10 03:44:19 +03:00
|
|
|
constants: { QueueUrl, requestId }
|
2018-05-09 21:27:08 +03:00
|
|
|
} = global;
|
2018-05-04 23:01:45 +03:00
|
|
|
|
2018-05-31 03:35:51 +03:00
|
|
|
const Metrics = require("../lib/metrics");
|
2018-05-09 21:27:08 +03:00
|
|
|
const accept = require("./accept");
|
2018-05-04 23:01:45 +03:00
|
|
|
|
|
|
|
describe("functions/accept.post", () => {
|
2018-05-31 03:35:51 +03:00
|
|
|
let metricsStub;
|
|
|
|
|
2018-05-04 23:01:45 +03:00
|
|
|
beforeEach(() => {
|
2018-05-09 21:27:08 +03:00
|
|
|
global.resetMocks();
|
2018-05-07 23:34:17 +03:00
|
|
|
process.env.ENABLE_DEV_AUTH = "1";
|
|
|
|
process.env.DISABLE_AUTH_CACHE = "1";
|
2018-05-31 03:35:51 +03:00
|
|
|
metricsStub = sinon.stub(Metrics, "newItem");
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
metricsStub.restore();
|
2018-05-04 23:01:45 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
describe("Hawk authentication", () => {
|
|
|
|
const expectHawkUnauthorized = result => {
|
|
|
|
expect(result.statusCode).to.equal(401);
|
|
|
|
expect(result.headers["WWW-Authenticate"]).to.equal("Hawk");
|
|
|
|
};
|
|
|
|
|
|
|
|
it("responds with 401 Unauthorized with disabled dev credentials", async () => {
|
|
|
|
process.env.ENABLE_DEV_AUTH = null;
|
2018-05-07 23:34:17 +03:00
|
|
|
|
2018-05-04 23:01:45 +03:00
|
|
|
const id = "devuser";
|
|
|
|
const { key, algorithm } = DEV_CREDENTIALS[id];
|
|
|
|
const result = await acceptPost({
|
|
|
|
httpMethod: "POST",
|
|
|
|
proto: "https",
|
|
|
|
host: "example.com",
|
|
|
|
port: 443,
|
|
|
|
path: "/prod/accept",
|
|
|
|
id,
|
|
|
|
key,
|
|
|
|
algorithm
|
|
|
|
});
|
|
|
|
expectHawkUnauthorized(result);
|
|
|
|
});
|
|
|
|
|
|
|
|
it("responds with 401 Unauthorized with bad id", async () => {
|
|
|
|
const badid = "somerando";
|
|
|
|
const key = "realkey";
|
|
|
|
|
2018-05-09 21:27:08 +03:00
|
|
|
mocks.getItem.returns(makePromiseFn({}));
|
2018-05-04 23:01:45 +03:00
|
|
|
|
|
|
|
const result = await acceptPost({
|
|
|
|
httpMethod: "POST",
|
|
|
|
proto: "https",
|
|
|
|
host: "example.com",
|
|
|
|
port: 443,
|
|
|
|
path: "/prod/accept",
|
|
|
|
id: badid,
|
|
|
|
key,
|
|
|
|
algorithm: DEFAULT_HAWK_ALGORITHM
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(mocks.getItem.lastCall.args[0]).to.deep.equal({
|
|
|
|
TableName: CREDENTIALS_TABLE,
|
|
|
|
Key: { id: badid },
|
|
|
|
AttributesToGet: ["key", "algorithm"]
|
|
|
|
});
|
|
|
|
expectHawkUnauthorized(result);
|
|
|
|
});
|
|
|
|
|
|
|
|
it("responds with 401 Unauthorized with bad key", async () => {
|
|
|
|
const id = "realuser";
|
|
|
|
const key = "realkey";
|
|
|
|
const badkey = "badkey";
|
|
|
|
const algorithm = "sha256";
|
|
|
|
|
2018-05-09 21:27:08 +03:00
|
|
|
mocks.getItem.returns(makePromiseFn({ Item: { key, algorithm } }));
|
2018-05-04 23:01:45 +03:00
|
|
|
|
|
|
|
const result = await acceptPost({
|
|
|
|
httpMethod: "POST",
|
|
|
|
proto: "https",
|
|
|
|
host: "example.com",
|
|
|
|
port: 443,
|
|
|
|
path: "/prod/accept",
|
|
|
|
id,
|
|
|
|
key: badkey,
|
|
|
|
algorithm: DEFAULT_HAWK_ALGORITHM
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(mocks.getItem.lastCall.args[0]).to.deep.equal({
|
|
|
|
TableName: CREDENTIALS_TABLE,
|
|
|
|
Key: { id },
|
|
|
|
AttributesToGet: ["key", "algorithm"]
|
|
|
|
});
|
|
|
|
expectHawkUnauthorized(result);
|
|
|
|
});
|
|
|
|
|
2018-05-07 23:34:17 +03:00
|
|
|
it("responds with 201 Created with enabled dev credentials", async () => {
|
2018-05-04 23:01:45 +03:00
|
|
|
const id = "devuser";
|
|
|
|
const { key, algorithm } = DEV_CREDENTIALS[id];
|
2018-05-07 23:34:17 +03:00
|
|
|
|
2018-05-04 23:01:45 +03:00
|
|
|
const result = await acceptPost({
|
|
|
|
httpMethod: "POST",
|
|
|
|
proto: "https",
|
|
|
|
host: "example.com",
|
|
|
|
port: 443,
|
|
|
|
path: "/prod/accept",
|
|
|
|
id,
|
|
|
|
key,
|
|
|
|
algorithm
|
|
|
|
});
|
|
|
|
|
2018-05-07 23:34:17 +03:00
|
|
|
// Dev credentials don't hit the database
|
|
|
|
expect(mocks.getItem.notCalled).to.be.true;
|
|
|
|
expect(result.statusCode).to.equal(201);
|
2018-05-04 23:01:45 +03:00
|
|
|
});
|
|
|
|
|
2018-05-07 23:34:17 +03:00
|
|
|
it("responds with 201 Created with real valid credentials", async () => {
|
2018-05-04 23:01:45 +03:00
|
|
|
const id = "realuser";
|
|
|
|
const key = "realkey";
|
|
|
|
const algorithm = "sha256";
|
|
|
|
|
2018-05-09 21:27:08 +03:00
|
|
|
mocks.getItem.returns(makePromiseFn({ Item: { key, algorithm } }));
|
2018-05-04 23:01:45 +03:00
|
|
|
|
|
|
|
const result = await acceptPost({
|
|
|
|
httpMethod: "POST",
|
|
|
|
proto: "https",
|
|
|
|
host: "example.com",
|
|
|
|
port: 443,
|
|
|
|
path: "/prod/accept",
|
|
|
|
id,
|
|
|
|
key,
|
|
|
|
algorithm
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(mocks.getItem.lastCall.args[0]).to.deep.equal({
|
|
|
|
TableName: CREDENTIALS_TABLE,
|
|
|
|
Key: { id },
|
|
|
|
AttributesToGet: ["key", "algorithm"]
|
|
|
|
});
|
2018-05-07 23:34:17 +03:00
|
|
|
expect(result.statusCode).to.equal(201);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe("Content submission", () => {
|
|
|
|
it("responds with 400 if missing a required field", async () => {
|
|
|
|
const id = "devuser";
|
|
|
|
const { key, algorithm } = DEV_CREDENTIALS[id];
|
|
|
|
const body = Object.assign({}, DEFAULT_POST_BODY);
|
|
|
|
delete body.image;
|
|
|
|
|
2018-05-31 03:35:51 +03:00
|
|
|
process.env.METRICS_URL = "https://example.com";
|
|
|
|
|
2018-05-07 23:34:17 +03:00
|
|
|
const result = await acceptPost({
|
|
|
|
httpMethod: "POST",
|
|
|
|
proto: "https",
|
|
|
|
host: "example.com",
|
|
|
|
port: 443,
|
|
|
|
path: "/prod/accept",
|
|
|
|
id,
|
|
|
|
key,
|
|
|
|
algorithm,
|
|
|
|
body
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result.statusCode).to.equal(400);
|
|
|
|
expect(JSON.parse(result.body).error).to.equal(
|
|
|
|
'Required "image" is missing'
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it("accepts a properly authorized image submission", async () => {
|
|
|
|
const id = "devuser";
|
|
|
|
const { key, algorithm } = DEV_CREDENTIALS[id];
|
|
|
|
const imageContent = "1234";
|
|
|
|
const imageContentType = "image/jpeg";
|
|
|
|
const body = Object.assign({}, DEFAULT_POST_BODY, {
|
|
|
|
image: {
|
|
|
|
filename: "image.jpg",
|
|
|
|
contentType: imageContentType,
|
|
|
|
content: imageContent
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = await acceptPost({
|
|
|
|
httpMethod: "POST",
|
|
|
|
proto: "https",
|
|
|
|
host: "example.com",
|
|
|
|
port: 443,
|
|
|
|
path: "/prod/accept",
|
|
|
|
id,
|
|
|
|
key,
|
|
|
|
algorithm,
|
|
|
|
body
|
|
|
|
});
|
|
|
|
|
2018-05-24 23:30:18 +03:00
|
|
|
const imageKey = `image-${requestId}`;
|
|
|
|
|
2018-05-07 23:34:17 +03:00
|
|
|
expect(mocks.putObject.args[0][0]).to.deep.equal({
|
|
|
|
Bucket: CONTENT_BUCKET,
|
2018-05-24 23:30:18 +03:00
|
|
|
Key: imageKey,
|
2018-05-07 23:34:17 +03:00
|
|
|
Body: new Buffer(imageContent),
|
|
|
|
ContentType: imageContentType
|
|
|
|
});
|
|
|
|
expect(mocks.getQueueUrl.args[0][0]).to.deep.equal({
|
|
|
|
QueueName: QUEUE_NAME
|
|
|
|
});
|
|
|
|
|
|
|
|
const message = mocks.sendMessage.args[0][0];
|
|
|
|
const messageBody = JSON.parse(message.MessageBody);
|
|
|
|
|
|
|
|
expect(message.QueueUrl).to.equal(QueueUrl);
|
2018-05-24 23:30:18 +03:00
|
|
|
expect("datestamp" in messageBody).to.be.true;
|
|
|
|
expect(messageBody.upstreamServiceUrl).to.equal(UPSTREAM_SERVICE_URL);
|
|
|
|
expect(messageBody.id).to.equal(requestId);
|
|
|
|
expect(messageBody.user).to.equal(id);
|
|
|
|
["negative_uri", "positive_uri", "positive_email", "notes"].forEach(
|
|
|
|
name => expect(messageBody[name]).to.equal(body[name])
|
|
|
|
);
|
|
|
|
expect(messageBody.image).to.equal(imageKey);
|
|
|
|
|
|
|
|
expect(mocks.putObject.args[1][0]).to.deep.equal({
|
|
|
|
Bucket: CONTENT_BUCKET,
|
|
|
|
Key: `${imageKey}-request.json`,
|
|
|
|
Body: message.MessageBody,
|
|
|
|
ContentType: "application/json"
|
|
|
|
});
|
|
|
|
|
2018-05-31 03:35:51 +03:00
|
|
|
expect(metricsStub.called).to.be.true;
|
|
|
|
expect(metricsStub.args[0][0]).to.deep.include({
|
|
|
|
consumer_name: id,
|
|
|
|
watchdog_id: requestId,
|
|
|
|
type: imageContentType
|
|
|
|
});
|
|
|
|
|
2018-05-07 23:34:17 +03:00
|
|
|
expect(result.statusCode).to.equal(201);
|
2018-05-04 23:01:45 +03:00
|
|
|
});
|
2018-05-04 20:36:06 +03:00
|
|
|
});
|
|
|
|
});
|
2018-05-07 23:34:17 +03:00
|
|
|
|
|
|
|
const DEFAULT_POST_BODY = {
|
|
|
|
negative_uri: "https://example.com/negative",
|
|
|
|
positive_uri: "https://example.com/positive",
|
|
|
|
positive_email: "positive@example.com",
|
|
|
|
notes: "foobar",
|
|
|
|
image: {
|
|
|
|
filename: "image.jpg",
|
|
|
|
contentType: "image/jpeg",
|
|
|
|
content: "1234123412341234"
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
async function acceptPost({
|
|
|
|
httpMethod,
|
|
|
|
proto,
|
|
|
|
host,
|
|
|
|
port,
|
|
|
|
path,
|
|
|
|
id,
|
|
|
|
key,
|
|
|
|
algorithm,
|
|
|
|
body = DEFAULT_POST_BODY
|
|
|
|
}) {
|
|
|
|
const { contentType, encodedBody } = buildBody(body);
|
|
|
|
const hawkResult = Hawk.client.header(
|
|
|
|
`${proto}://${host}:${port}${path}`,
|
|
|
|
httpMethod,
|
|
|
|
{ credentials: { id, key, algorithm } }
|
|
|
|
);
|
|
|
|
const headers = {
|
|
|
|
Host: host,
|
|
|
|
"X-Forwarded-Port": port,
|
|
|
|
"Content-Type": contentType,
|
|
|
|
Authorization: hawkResult.header
|
|
|
|
};
|
|
|
|
return accept.post(
|
|
|
|
{
|
|
|
|
path,
|
|
|
|
httpMethod,
|
|
|
|
headers,
|
|
|
|
body: encodedBody,
|
|
|
|
requestContext: { path, requestId }
|
|
|
|
},
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function buildBody(data) {
|
|
|
|
const boundary = "--------------------------065117214804889366770750";
|
|
|
|
const contentType = `multipart/form-data; boundary=${boundary}`;
|
|
|
|
|
|
|
|
const encString = (name, value) =>
|
|
|
|
`Content-Disposition: form-data; name="${name}"\r\n` +
|
|
|
|
"\r\n" +
|
|
|
|
value +
|
|
|
|
"\r\n";
|
|
|
|
|
|
|
|
const encFile = (name, { filename, contentType, content }) =>
|
|
|
|
`Content-Disposition: form-data; name="${name}"; filename="${filename}"\r\n` +
|
|
|
|
`Content-Type: ${contentType}\r\n` +
|
|
|
|
"\r\n" +
|
|
|
|
content +
|
|
|
|
"\r\n";
|
|
|
|
|
|
|
|
const encodedBody = [
|
|
|
|
`--${boundary}\r\n`,
|
|
|
|
Object.entries(data)
|
|
|
|
.map(
|
|
|
|
([name, value]) =>
|
|
|
|
typeof value == "string"
|
|
|
|
? encString(name, value)
|
|
|
|
: encFile(name, value)
|
|
|
|
)
|
|
|
|
.join("--" + boundary + "\r\n"),
|
|
|
|
`--${boundary}--`
|
|
|
|
].join("");
|
|
|
|
|
|
|
|
return { contentType, encodedBody };
|
|
|
|
}
|