Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
348ccd2
Changes background color of selected item
simonbs Oct 13, 2023
b5c1e48
Reduces logging of sensitive information
simonbs Oct 13, 2023
d10c40f
Removes greeting
simonbs Oct 13, 2023
1bfb432
Removes unused code
simonbs Oct 13, 2023
67ead10
Shows dividers when menu item is selected
simonbs Oct 13, 2023
4e309ed
Improves performance
simonbs Oct 19, 2023
1a9e480
Redesigns settings
simonbs Oct 19, 2023
a0430cb
Prevents dark mode
simonbs Oct 19, 2023
0b9c50a
Removes Shape logo
simonbs Oct 19, 2023
4785dec
Fixes scrolling
simonbs Oct 19, 2023
db4186d
Adds version and specification picker
simonbs Oct 19, 2023
efb4aa7
Removes welcome page
simonbs Oct 19, 2023
c40d433
Changes default color
simonbs Oct 19, 2023
24a4dc1
Adds edit button
simonbs Oct 19, 2023
c62400c
Simplifies project list
simonbs Oct 19, 2023
a978060
Removes unused imports
simonbs Oct 19, 2023
8796dd4
Fixes import
simonbs Oct 19, 2023
1887049
Removes unused variable
simonbs Oct 19, 2023
7dd6d35
Fixes build issue
simonbs Oct 19, 2023
8b4caa4
Adds engines
simonbs Oct 19, 2023
6b92fe1
Fixes flickering duck
simonbs Oct 19, 2023
74d9932
Makes project list scrollable
simonbs Oct 19, 2023
2c14e3b
Handles empty project list
simonbs Oct 19, 2023
e08a302
Select only project if none is selected
simonbs Oct 19, 2023
46644dc
Ensure URL is updated with selection
simonbs Oct 19, 2023
e330b32
Simplifies client boundary
simonbs Oct 20, 2023
748f306
Persists drawer state for duration of session
simonbs Oct 20, 2023
6d364c5
Passes error to content
simonbs Oct 20, 2023
a1e579b
Hides scroll indicator
simonbs Oct 20, 2023
f7729c4
Improves interface of getProjectSelection
simonbs Oct 20, 2023
d030315
Defaults to empty array of projects
simonbs Oct 20, 2023
099491c
Adds tests for ProjectSelection
simonbs Oct 20, 2023
f8d2cb9
Tests AccessTokenService
simonbs Oct 20, 2023
86c2791
Adds logo to README (#7)
simonbs Oct 20, 2023
11c71bd
Update test.yml
simonbs Oct 20, 2023
4b0513b
Update build.yml
simonbs Oct 20, 2023
1c50e9f
Update build.yml
simonbs Oct 20, 2023
0f4e2de
Update build.yml
simonbs Oct 20, 2023
5bdfe7f
Update README.md
simonbs Oct 20, 2023
8c81b01
Adds unit tests for navigation (#8)
simonbs Oct 20, 2023
9b8e85d
Makes editURL optional (#9)
simonbs Oct 20, 2023
fee128b
Shows version and specification as path in toolbar (#10)
simonbs Oct 20, 2023
f484a13
Fixes crash when no config exists (#11)
simonbs Oct 20, 2023
05574e6
Removes openapi suffix from default name (#13)
simonbs Oct 20, 2023
a0de436
Makes app icons rounded (#12)
simonbs Oct 20, 2023
20d6ded
Fixes hooks called multiple times (#14)
simonbs Oct 21, 2023
0f5fb58
Changes background color of selected item (#6) (#15)
simonbs Oct 21, 2023
a93ec77
Merge branch 'main' into develop
simonbs Oct 21, 2023
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
18 changes: 16 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,24 @@ name: Build
on:
workflow_dispatch: {}
pull_request:
branches: [main]
branches: [main, develop]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
AUTH0_SECRET: 336f7a926310cff425cea29556dce2a98859b8d234aa27968696c2e6f1cb7d34
AUTH0_BASE_URL: http://dev.local:3000
AUTH0_ISSUER_BASE_URL: https://shape-docs-dev.eu.auth0.com
AUTH0_CLIENT_ID: this-is-our-client-id
AUTH0_CLIENT_SECRET: this-is-our-client-secret
AUTH0_MANAGEMENT_DOMAIN: shape-docs-dev.eu.auth0.com
AUTH0_MANAGEMENT_CLIENT_ID: this-is-our-management-client-id
AUTH0_MANAGEMENT_CLIENT_SECRET: this-is-our-management-client-secret
GITHUB_CLIENT_ID: this-is-our-github-client-id
GITHUB_CLIENT_SECRET: this-is-our-github-client-secret
GITHUB_APP_ID: 12345
GITHUB_PRIVATE_KEY_BASE_64: LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNCS2NaL3UyL3dubDA4R1BjeXdiSVc2QVdKdnhVL2o0V0g2UnkxODVPSHZDd0FBQUpBYW01MzdHcHVkCit3QUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQktjWi91Mi93bmwwOEdQY3l3YklXNkFXSnZ4VS9qNFdINlJ5MTg1T0h2Q3cKQUFBRUJ6T2htRGpuY3R0QSt3ai9USHRnWnN6MklRWjdLM2ovb0Z1OEZBNTMxTDhrcHhuKzdiL0NlWFR3WTl6TEJzaGJvQgpZbS9GVCtQaFlmcEhMWHprNGU4TEFBQUFDMlp2YjBCb1lYQmxMbVJyQVFJPQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K
GITHUB_WEBHOOK_SECRET: super-duper-secret
jobs:
build:
name: Build
Expand All @@ -16,7 +30,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
- name: Install Dependencies
run: npm install
- name: Build
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Test
on:
workflow_dispatch: {}
pull_request:
branches: [main]
branches: [main, develop]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
<p align="center">
<img height="300" src="/logo.png">
</p>

# shape-docs

Portal displaying our projects that are documented with OpenAPI.

[![Build](https://github.com/shapehq/shape-docs/actions/workflows/build.yml/badge.svg)](https://github.com/shapehq/shape-docs/actions/workflows/build.yml)
[![Test](https://github.com/shapehq/shape-docs/actions/workflows/test.yml/badge.svg)](https://github.com/shapehq/shape-docs/actions/workflows/test.yml)

## Running the App Locally

Create a file named `.env.local` in the root of the project with the following contents. Make sure to replace any placeholders and generate a random secret using OpenSSL.

```
SHAPE_DOCS_BASE_URL='https://docs.shapetools.io'
AUTH0_SECRET='use [openssl rand -hex 32] to generate a 32 bytes value'
AUTH0_BASE_URL='http://dev.local:3000'
AUTH0_ISSUER_BASE_URL='https://shape-docs-dev.eu.auth0.com'
Expand All @@ -22,9 +28,31 @@ GITHUB_CLIENT_SECRET='GitHub App client secret'
GITHUB_APP_ID='the GitHub App id'
GITHUB_PRIVATE_KEY_BASE_64='base 64 encoded version of the private key'
GITHUB_WEBHOOK_SECRET='preshared secret also put in app conf in GitHub'
GITHUB_WEBHOK_REPOSITORY_ALLOWLIST=''
GITHUB_WEBHOK_REPOSITORY_DISALLOWLIST=''
```

Note that you need the following two Auth0 apps.
Each environment variable is described in the table below.

|Environment Variable|Description|
|-|-|
|SHAPE_DOCS_BASE_URL|The URL where Shape Docs is hosted.|
|AUTH0_SECRET|A long secret value used to encrypt the session cookie. Generate it using `openssl rand -hex 32`.|AUTH0_BASE_URL|The base URL of your Auth0 application. `http://dev.local:3000` during development.|
|AUTH0_ISSUER_BASE_URL|The URL of your Auth0 tenant domain.|
|AUTH0_CLIENT_ID|The client ID of your default Auth0 application.|
|AUTH0_CLIENT_SECRET|The client secret of your default Auth0 application.|
|AUTH0_MANAGEMENT_DOMAIN|The URL of your Auth0 tenant domain. It is key that this does not contain "http" or "https".|
|AUTH0_MANAGEMENT_CLIENT_ID|The client ID of your Auth0 Machine to Machine application.|
|AUTH0_MANAGEMENT_CLIENT_SECRET|The client secret of your Machine to Machine Auth0 application.|
|GITHUB_CLIENT_ID|The client ID of your GitHub app.|
|GITHUB_CLIENT_SECRET|The client secret of your GitHub app.|
|GITHUB_APP_ID|The ID of your GitHub app.|
|GITHUB_PRIVATE_KEY_BASE_64|Your GitHub app's private key encoded to base 64. Can be created using `cat my-key.pem | base64 | pbcopy`.|
|GITHUB_WEBHOOK_SECRET|Secret shared with the GitHub app to validate a webhook call.|
|GITHUB_WEBHOK_REPOSITORY_ALLOWLIST|Comma-separated list of repositories from which webhook calls should be accepted. Leave empty to accept calls from all repositories.|
|GITHUB_WEBHOK_REPOSITORY_DISALLOWLIST|Comma-separated list of repositories from which webhook calls should be ignored. The list of disallowed repositories takes precedence over the list of allowed repositories.|

You need the following two Auth0 apps.

| |Type|Description|
|-|-|-|
Expand Down
104 changes: 104 additions & 0 deletions __test__/auth/AccessTokenService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import AccessTokenService from "../../src/features/auth/domain/AccessTokenService"
import { IOAuthToken } from "../../src/features/auth/domain/IOAuthTokenRepository"

test("It reads the access token from the repository", async () => {
const sut = new AccessTokenService({
async getOAuthToken() {
return {
accessToken: "foo",
refreshToken: "bar",
accessTokenExpiryDate: new Date(new Date().getTime() + 3600 * 1000),
refreshTokenExpiryDate: new Date(new Date().getTime() + 3600 * 1000)
}
},
async storeOAuthToken(_token: IOAuthToken) {}
}, {
async refreshAccessToken(_refreshToken: string) {
return {
accessToken: "foo",
refreshToken: "bar",
accessTokenExpiryDate: new Date(new Date().getTime() + 3600 * 1000),
refreshTokenExpiryDate: new Date(new Date().getTime() + 3600 * 1000)
}
}
})
const accessToken = await sut.getAccessToken()
expect(accessToken).toBe("foo")
})

test("It refreshes an expired access token", async () => {
const sut = new AccessTokenService({
async getOAuthToken() {
return {
accessToken: "old",
refreshToken: "bar",
accessTokenExpiryDate: new Date(new Date().getTime() - 3600 * 1000),
refreshTokenExpiryDate: new Date(new Date().getTime() + 3600 * 1000)
}
},
async storeOAuthToken(_token: IOAuthToken) {}
}, {
async refreshAccessToken(_refreshToken: string) {
return {
accessToken: "new",
refreshToken: "bar",
accessTokenExpiryDate: new Date(new Date().getTime() + 3600 * 1000),
refreshTokenExpiryDate: new Date(new Date().getTime() + 3600 * 1000)
}
}
})
const accessToken = await sut.getAccessToken()
expect(accessToken).toBe("new")
})

test("It stores the refreshed access token", async () => {
let didStoreRefreshedToken = false
const sut = new AccessTokenService({
async getOAuthToken() {
return {
accessToken: "old",
refreshToken: "bar",
accessTokenExpiryDate: new Date(new Date().getTime() - 3600 * 1000),
refreshTokenExpiryDate: new Date(new Date().getTime() + 3600 * 1000)
}
},
async storeOAuthToken(_token: IOAuthToken) {
didStoreRefreshedToken = true
}
}, {
async refreshAccessToken(_refreshToken: string) {
return {
accessToken: "new",
refreshToken: "bar",
accessTokenExpiryDate: new Date(new Date().getTime() + 3600 * 1000),
refreshTokenExpiryDate: new Date(new Date().getTime() + 3600 * 1000)
}
}
})
await sut.getAccessToken()
expect(didStoreRefreshedToken).toBeTruthy()
})

test("It errors when the refresh token has expired", async () => {
const sut = new AccessTokenService({
async getOAuthToken() {
return {
accessToken: "old",
refreshToken: "bar",
accessTokenExpiryDate: new Date(new Date().getTime() - 3600 * 1000),
refreshTokenExpiryDate: new Date(new Date().getTime() - 3600 * 1000)
}
},
async storeOAuthToken(_token: IOAuthToken) {}
}, {
async refreshAccessToken(_refreshToken: string) {
return {
accessToken: "new",
refreshToken: "bar",
accessTokenExpiryDate: new Date(new Date().getTime() + 3600 * 1000),
refreshTokenExpiryDate: new Date(new Date().getTime() + 3600 * 1000)
}
}
})
await expect(sut.getAccessToken()).rejects.toThrow()
})
29 changes: 0 additions & 29 deletions __test__/auth/IdentityAccessTokenProvider.test.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import ExistingCommentCheckingPullRequestEventHandler from "../../src/features/hooks/domain/ExistingCommentCheckingPullRequestEventHandler"

test("It fetches comments from the repository", async () => {
let didFetchComments = false
const sut = new ExistingCommentCheckingPullRequestEventHandler({
async pullRequestOpened(_event) {}
}, {
async getComments(_operation) {
didFetchComments = true
return []
},
async addComment(_operation) {}
}, "https://docs.shapetools.io")
await sut.pullRequestOpened({
appInstallationId: 42,
repositoryOwner: "shapehq",
repositoryName: "foo",
ref: "bar",
pullRequestNumber: 1337
})
expect(didFetchComments).toBeTruthy()
})

test("It does calls decorated event handler if a comment does not exist in the repository", async () => {
let didCallEventHandler = false
const sut = new ExistingCommentCheckingPullRequestEventHandler({
async pullRequestOpened(_event) {
didCallEventHandler = true
}
}, {
async getComments(_operation) {
return []
},
async addComment(_operation) {}
}, "https://docs.shapetools.io")
await sut.pullRequestOpened({
appInstallationId: 42,
repositoryOwner: "shapehq",
repositoryName: "foo",
ref: "bar",
pullRequestNumber: 1337
})
expect(didCallEventHandler).toBeTruthy()
})

test("It does not call the event handler if a comment already exists in the repository", async () => {
let didCallEventHandler = false
const sut = new ExistingCommentCheckingPullRequestEventHandler({
async pullRequestOpened(_event) {
didCallEventHandler = true
}
}, {
async getComments(_operation) {
return [{
body: "The documentation is available on https://docs.shapetools.io",
isFromBot: true
}]
},
async addComment(_operation) {}
}, "https://docs.shapetools.io")
await sut.pullRequestOpened({
appInstallationId: 42,
repositoryOwner: "shapehq",
repositoryName: "foo",
ref: "bar",
pullRequestNumber: 1337
})
expect(didCallEventHandler).toBeFalsy()
})

test("It calls the event handler if a comment exists matching the needle domain but that comment is not from a bot", async () => {
let didCallEventHandler = false
const sut = new ExistingCommentCheckingPullRequestEventHandler({
async pullRequestOpened(_event) {
didCallEventHandler = true
}
}, {
async getComments(_operation) {
return [{
body: "The documentation is available on https://docs.shapetools.io",
isFromBot: false
}]
},
async addComment(_operation) {}
}, "https://docs.shapetools.io")
await sut.pullRequestOpened({
appInstallationId: 42,
repositoryOwner: "shapehq",
repositoryName: "foo",
ref: "bar",
pullRequestNumber: 1337
})
expect(didCallEventHandler).toBeTruthy()
})

test("It calls the event handler if the repository contains a comment from a bot but that comment does not contain the needle domain", async () => {
let didCallEventHandler = false
const sut = new ExistingCommentCheckingPullRequestEventHandler({
async pullRequestOpened(_event) {
didCallEventHandler = true
}
}, {
async getComments(_operation) {
return [{
body: "Hello world!",
isFromBot: true
}]
},
async addComment(_operation) {}
}, "https://docs.shapetools.io")
await sut.pullRequestOpened({
appInstallationId: 42,
repositoryOwner: "shapehq",
repositoryName: "foo",
ref: "bar",
pullRequestNumber: 1337
})
expect(didCallEventHandler).toBeTruthy()
})
8 changes: 8 additions & 0 deletions __test__/hooks/GitHubCommentFactory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import GitHubCommentFactory from "../../src/features/hooks/domain/GitHubCommentFactory"

test("It includes a link to the documentation", async () => {
const text = GitHubCommentFactory.makeDocumentationPreviewReadyComment(
"https://docs.shapetools.io/foo/bar"
)
expect(text).toContain("https://docs.shapetools.io/foo/bar")
})
Loading