Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
jobs:
build:
name: Build and test
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
Expand All @@ -22,7 +22,9 @@ jobs:
- name: Setup AWS credentials
run: |
mkdir ~/.aws
echo -e "[default]\naws_access_key_id=''\naws_secret_access_key=''" > ~/.aws/credentials
# DynamoDB Local now validates credentials and rejects empty/invalid
# ones with UnrecognizedClientException, so use non-empty placeholders.
echo -e "[default]\naws_access_key_id=fakeMyKeyId\naws_secret_access_key=fakeSecretAccessKey" > ~/.aws/credentials
- name: Setup Serverless Offline & Run Tests
run: yarn ci
- name: Coveralls
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
jobs:
analyse:
name: Analyse
runs-on: ubuntu-20.04
runs-on: ubuntu-latest

steps:
- name: Checkout repository
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
jobs:
deploy-dev:
name: Deploy to dev
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
environment: dev
steps:
- uses: actions/checkout@v2
Expand All @@ -26,7 +26,7 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
deploy-prod:
name: Deploy to prod
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
environment: prod
steps:
- uses: actions/checkout@v2
Expand Down
57 changes: 5 additions & 52 deletions src/handlers/slack/infra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import LockBot from "../../lock-bot";
import DynamoDBLockRepo from "../../storage/dynamodb-lock-repo";
import TokenAuthorizer from "../../token-authorizer";
import DynamoDBAccessTokenRepo from "../../storage/dynamodb-token-repo";
import createInstallationStore from "./installation-store";

const documentClient = new DocumentClient();

Expand All @@ -20,58 +21,10 @@ export const expressReceiver = new ExpressReceiver({
stateSecret: env.get("STATE_SECRET").required().asString(),
scopes: ["commands"],
processBeforeResponse: true,
installationStore: {
storeInstallation: async (installation, logger) => {
if (installation.isEnterpriseInstall && installation.enterprise) {
logger?.error("Enterprise storeInstallation attempt failed.");
throw new Error("Enterprise installation not supported");
} else if (installation.team) {
await documentClient
.put({
TableName: installationsTableName,
Item: {
Team: installation.team.id,
Installation: installation,
},
})
.promise();
const { team, user, bot } = installation;
logger?.info("Installation stored.", {
team,
userId: user.id,
botScopes: bot?.scopes,
});
} else {
throw new Error("Failed to store installation");
}
},
fetchInstallation: async (installQuery, logger) => {
if (
installQuery.isEnterpriseInstall &&
installQuery.enterpriseId !== undefined
) {
logger?.error("Enterprise fetchInstallation attempt failed.");
throw new Error("Enterprise installation not supported");
} else if (installQuery.teamId !== undefined) {
const result = await documentClient
.get({
TableName: installationsTableName,
Key: { Team: installQuery.teamId },
})
.promise();
const installation = result.Item?.Installation;
const { team, user, bot } = installation;
logger?.info("Installation fetched.", {
team,
userId: user.id,
botScopes: bot?.scopes,
});
return Promise.resolve(installation);
} else {
throw new Error("Failed to fetch installation");
}
},
},
installationStore: createInstallationStore(
documentClient,
installationsTableName
),
});

export const app = new App({
Expand Down
60 changes: 60 additions & 0 deletions src/handlers/slack/installation-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { DocumentClient } from "aws-sdk/clients/dynamodb";
import { InstallationStore } from "@slack/bolt";

const createInstallationStore = (
documentClient: DocumentClient,
installationsTableName: string
): InstallationStore => ({
storeInstallation: async (installation, logger) => {
if (installation.isEnterpriseInstall && installation.enterprise) {
logger?.error("Enterprise storeInstallation attempt failed.");
throw new Error("Enterprise installation not supported");
} else if (installation.team) {
await documentClient
.put({
TableName: installationsTableName,
Item: {
Team: installation.team.id,
Installation: installation,
},
})
.promise();
const { team, user, bot } = installation;
logger?.info("Installation stored.", {
team,
userId: user.id,
botScopes: bot?.scopes,
});
} else {
throw new Error("Failed to store installation");
}
},
fetchInstallation: async (installQuery, logger) => {
if (
installQuery.isEnterpriseInstall &&
installQuery.enterpriseId !== undefined
) {
logger?.error("Enterprise fetchInstallation attempt failed.");
throw new Error("Enterprise installation not supported");
} else if (installQuery.teamId !== undefined) {

Check warning on line 39 in src/handlers/slack/installation-store.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=connorads_lockbot&issues=AZ55QuOeEMqJBb_RbHxH&open=AZ55QuOeEMqJBb_RbHxH&pullRequest=208
const result = await documentClient
.get({
TableName: installationsTableName,
Key: { Team: installQuery.teamId },
})
.promise();
const installation = result.Item?.Installation;
const { team, user, bot } = installation;
logger?.info("Installation fetched.", {
team,
userId: user.id,
botScopes: bot?.scopes,
});
return Promise.resolve(installation);

Check failure on line 53 in src/handlers/slack/installation-store.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `return value` over `return Promise.resolve(value)`.

See more on https://sonarcloud.io/project/issues?id=connorads_lockbot&issues=AZ55QuOeEMqJBb_RbHxI&open=AZ55QuOeEMqJBb_RbHxI&pullRequest=208
} else {
throw new Error("Failed to fetch installation");
}
},
});

export default createInstallationStore;
28 changes: 28 additions & 0 deletions src/handlers/swagger/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import express from "express";

const createSwaggerApp = (
stage: string,
swaggerHtml: string,
openApiJson: Record<string, unknown>
) => {
const openApiWithServers = {
...openApiJson,
servers: [{ url: `/${stage}` }],
};

const app = express();

app.get("/api-docs", async (req, res) => {
res.set("Content-Type", "text/html; charset=utf-8");
res.send(swaggerHtml);
});

app.get(["/openapi.json", "/api-docs/openapi.json"], async (req, res) => {
res.set("Content-Type", "application/json; charset=utf-8");
res.send(openApiWithServers);
});

return app;
};

export default createSwaggerApp;
17 changes: 3 additions & 14 deletions src/handlers/swagger/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import express from "express";
import * as env from "env-var";
import serverlessExpress from "@vendia/serverless-express";
import createSwaggerApp from "./app";

// swagger.html is inlined as text by webpack's text-loader (webpack.config.js).
const swaggerHtml = require("./swagger.html");
const openApiJson = require("./openapi.json");

const stage = env.get("SERVERLESS_STAGE").required().asString();
openApiJson.servers = [{ url: `/${stage}` }];

const app = express();

app.get("/api-docs", async (req, res) => {
res.set("Content-Type", "text/html; charset=utf-8");
res.send(swaggerHtml);
});

app.get(["/openapi.json", "/api-docs/openapi.json"], async (req, res) => {
res.set("Content-Type", "application/json; charset=utf-8");
res.send(openApiJson);
});
const app = createSwaggerApp(stage, swaggerHtml, openApiJson);

exports.handler = serverlessExpress({ app });
55 changes: 55 additions & 0 deletions test/command-parsers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
parseUnlock,
getFirstParam,
} from "../src/handlers/slack/command-parsers";

describe("parseUnlock", () => {
test("parses a resource with no options as not forced", () => {
expect(parseUnlock("staging")).toEqual({
resource: "staging",
force: false,
});
});

test("parses the force option", () => {
expect(parseUnlock("staging force")).toEqual({
resource: "staging",
force: true,
});
});

test("treats an unrecognised second word as not forced", () => {
expect(parseUnlock("staging please")).toEqual({
resource: "staging",
force: false,
});
});

test("only the exact lowercase word 'force' enables force", () => {
expect(parseUnlock("staging FORCE")).toEqual({
resource: "staging",
force: false,
});
});

test("returns an empty resource for empty input", () => {
expect(parseUnlock("")).toEqual({
resource: "",
force: false,
});
});
});

describe("getFirstParam", () => {
test("returns the first space-separated word", () => {
expect(getFirstParam("staging extra args")).toEqual("staging");
});

test("returns the whole string when there is a single word", () => {
expect(getFirstParam("staging")).toEqual("staging");
});

test("returns an empty string for empty input", () => {
expect(getFirstParam("")).toEqual("");
});
});
Loading
Loading