Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(repo): add status code to HTTPError type #135

Merged
merged 4 commits into from
Apr 20, 2024
Merged
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
1 change: 0 additions & 1 deletion .github/workflows/pr-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,3 @@ jobs:
CAMUNDA_CONSOLE_CLIENT_SECRET: ${{ secrets.CAMUNDA_CONSOLE_CLIENT_SECRET }}
CAMUNDA_CONSOLE_BASE_URL: ${{ secrets.CAMUNDA_CONSOLE_BASE_URL }}
CAMUNDA_CONSOLE_OAUTH_AUDIENCE: ${{ secrets.CAMUNDA_CONSOLE_OAUTH_AUDIENCE}}
CAMUNDA_OAUTH_TOKEN_REFRESH_THRESHOLD_MS: 10000 #89: Intermittent 401 unauthorised in integration tests
1 change: 0 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ jobs:
CAMUNDA_CONSOLE_CLIENT_SECRET: ${{ secrets.CAMUNDA_CONSOLE_CLIENT_SECRET }}
CAMUNDA_CONSOLE_BASE_URL: ${{ secrets.CAMUNDA_CONSOLE_BASE_URL }}
CAMUNDA_CONSOLE_OAUTH_AUDIENCE: ${{ secrets.CAMUNDA_CONSOLE_OAUTH_AUDIENCE}}
CAMUNDA_OAUTH_TOKEN_REFRESH_THRESHOLD_MS: 10000 #89: Intermittent 401 unauthorised in integration tests

tag-and-publish:
needs:
Expand Down
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,6 @@ const c8 = new Camunda8({

If the cache directory does not exist, the SDK will attempt to create it (recursively). If the SDK is unable to create it, or the directory exists but is not writeable by your application, the SDK will throw an exception.

### Token refresh

Token refresh timing relative to expiration is controlled by the `CAMUNDA_OAUTH_TOKEN_REFRESH_THRESHOLD_MS` value. By default, this is 1000ms. Tokens are renewed this amount of time before they expire.

If you experience intermittent `401: Unauthorized` errors, this may not be sufficient time to refresh the token before it expires in your infrastructure. Increase this value to force a token to be refreshed before it expires.

## Connection configuration examples

### Self-Managed
Expand Down
2 changes: 0 additions & 2 deletions docker/docker-compose-modeler.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
#
# Note: this file is using Mailpit to simulate a mail server

version: "2.4"

services:
modeler-db:
container_name: modeler-db
Expand Down
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"devDependencies": {
"@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3",
"@mokuteki/jwt": "^1.0.2",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@sitapati/testcontainers": "^2.8.1",
Expand Down Expand Up @@ -121,6 +122,7 @@
"debug": "^4.3.4",
"fast-xml-parser": "^4.1.3",
"got": "^11.8.6",
"jwt-decode": "^4.0.0",
"lodash.mergewith": "^4.6.2",
"long": "^4.0.0",
"lossless-json": "^4.0.1",
Expand Down
93 changes: 26 additions & 67 deletions src/__tests__/oauth/OAuthProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,22 @@ import fs from 'fs'
import http from 'http'
import path from 'path'

import { HS256Strategy, JSONWebToken } from '@mokuteki/jwt'

import { EnvironmentSetup } from '../../lib'
import { OAuthProvider } from '../../oauth'

const strategy = new HS256Strategy({
ttl: 30000,
secret: 'YOUR_SECRET',
})

const jwt = new JSONWebToken(strategy)
const payload = { id: 1 }

const access_token = jwt.generate(payload)
const access_token2 = jwt.generate(payload)

jest.setTimeout(10000)
let server: http.Server

Expand Down Expand Up @@ -176,8 +189,8 @@ test('In-memory cache is populated and evicted after timeout', (done) => {
ZEEBE_CLIENT_ID: 'clientId6',
ZEEBE_CLIENT_SECRET: 'clientSecret',
CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3002}`,
CAMUNDA_OAUTH_TOKEN_REFRESH_THRESHOLD_MS: 0,
CAMUNDA_TOKEN_DISK_CACHE_DISABLE: true,
CAMUNDA_OAUTH_TOKEN_REFRESH_THRESHOLD_MS: 0,
},
})

Expand All @@ -193,9 +206,9 @@ test('In-memory cache is populated and evicted after timeout', (done) => {
req.on('end', () => {
res.writeHead(200, { 'Content-Type': 'application/json' })
const expiresIn = 2 // seconds
res.end(
`{"access_token": "${requestCount++}", "expires_in": ${expiresIn}}`
)
const token = requestCount % 2 === 0 ? access_token : access_token2
res.end(`{"access_token": "${token}", "expires_in": ${expiresIn}}`)
requestCount++
expect(body).toEqual(
'audience=token&client_id=clientId6&client_secret=clientSecret&grant_type=client_credentials'
)
Expand All @@ -205,67 +218,13 @@ test('In-memory cache is populated and evicted after timeout', (done) => {
.listen(serverPort3002)

o.getToken('ZEEBE').then(async (token) => {
expect(token).toBe('0')
expect(token).toBe(access_token)
await delay(500)
const token2 = await o.getToken('ZEEBE')
expect(token2).toBe('0')
expect(token2).toBe(access_token)
await delay(1600)
const token3 = await o.getToken('ZEEBE')
expect(token3).toBe('1')
done()
})
})

// Added test for https://github.com/camunda/camunda-8-js-sdk/issues/62
// "OAuth token refresh has a race condition"
test('In-memory cache is populated and evicted respecting CAMUNDA_OAUTH_TOKEN_REFRESH_THRESHOLD_MS', (done) => {
const delay = (timeout: number) =>
new Promise((res) => setTimeout(() => res(null), timeout))

const serverPort3009 = 3009
const o = new OAuthProvider({
config: {
CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token',
ZEEBE_CLIENT_ID: 'clientId7',
ZEEBE_CLIENT_SECRET: 'clientSecret',
CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3009}`,
CAMUNDA_OAUTH_TOKEN_REFRESH_THRESHOLD_MS: 2000,
CAMUNDA_TOKEN_DISK_CACHE_DISABLE: true,
},
})

let requestCount = 0
server = http
.createServer((req, res) => {
if (req.method === 'POST') {
let body = ''
req.on('data', (chunk) => {
body += chunk
})

req.on('end', () => {
res.writeHead(200, { 'Content-Type': 'application/json' })
const expiresIn = 5 // seconds
res.end(
`{"access_token": "${requestCount++}", "expires_in": ${expiresIn}}`
)
expect(body).toEqual(
'audience=token&client_id=clientId7&client_secret=clientSecret&grant_type=client_credentials'
)
})
}
})
.listen(serverPort3009)

o.flushFileCache()
o.getToken('ZEEBE').then(async (token) => {
expect(token).toBe('0')
await delay(500)
const token2 = await o.getToken('ZEEBE')
expect(token2).toBe('0')
await delay(3600)
const token3 = await o.getToken('ZEEBE')
expect(token3).toBe('1')
expect(token3).toBe(access_token2)
done()
})
})
Expand All @@ -290,7 +249,7 @@ test('Uses form encoding for request', (done) => {

req.on('end', () => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(`{"access_token": "token-content", "expires_in": "5"}`)
res.end(`{"access_token": "${access_token}", "expires_in": "5"}`)
server.close()
expect(body).toEqual(
'audience=operate.camunda.io&client_id=clientId8&client_secret=clientSecret&grant_type=client_credentials'
Expand Down Expand Up @@ -324,7 +283,7 @@ test('Uses a custom audience for an Operate token, if one is configured', (done)

req.on('end', () => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(`{"access_token": "token-content", "expires_in": "5"}`)
res.end(`{"access_token": "${access_token}", "expires_in": "5"}`)
server.close()
expect(body).toEqual(
'audience=custom.operate.audience&client_id=clientId9&client_secret=clientSecret&grant_type=client_credentials'
Expand All @@ -337,7 +296,7 @@ test('Uses a custom audience for an Operate token, if one is configured', (done)
o.getToken('OPERATE')
})

test('Passes scope, if provided', () => {
test.only('Passes scope, if provided', () => {
const serverPort3004 = 3004
const o = new OAuthProvider({
config: {
Expand All @@ -358,7 +317,7 @@ test('Passes scope, if provided', () => {

req.on('end', () => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(`{"access_token": "token-content", "expires_in": "5"}`)
res.end(`{"access_token": "${access_token}", "expires_in": "5"}`)

expect(body).toEqual(
'audience=token&client_id=clientId10&client_secret=clientSecret&grant_type=client_credentials&scope=scope'
Expand Down Expand Up @@ -392,7 +351,7 @@ test('Can get scope from environment', () => {

req.on('end', () => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(`{"access_token": "token-content", "expires_in": "5"}`)
res.end(`{"access_token": "${access_token}", "expires_in": "5"}`)

expect(body).toEqual(
'audience=token&client_id=clientId11&client_secret=clientSecret&grant_type=client_credentials&scope=scope2'
Expand Down Expand Up @@ -551,7 +510,7 @@ test('Passes no audience for Modeler API when self-hosted', (done) => {

req.on('end', () => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end('{"token": "something"}')
res.end(`{"token": "${access_token}"}`)
expect(body).toEqual(
'client_id=clientId17&client_secret=clientSecret&grant_type=client_credentials'
)
Expand Down
28 changes: 25 additions & 3 deletions src/__tests__/operate/operate-integration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { RESTError } from 'lib'
import { LosslessNumber } from 'lossless-json'

import {
HTTPError,
RESTError,
restoreZeebeLogging,
suppressZeebeLogging,
} from '../../lib'
import { OperateApiClient } from '../../operate'
import { ProcessDefinition, Query } from '../../operate/lib/OperateDto'
import { ZeebeGrpcClient } from '../../zeebe'

suppressZeebeLogging()

afterAll(async () => {
restoreZeebeLogging()
})

jest.setTimeout(15000)
describe('Operate Integration', () => {
xtest('It can get the Incident', async () => {
Expand Down Expand Up @@ -48,7 +59,14 @@ test('getJSONVariablesforProcess works', async () => {
foo: 'bar',
},
})
await new Promise((res) => setTimeout(() => res(null), 5000))

// Wait for Operate to catch up.
await new Promise((res) => setTimeout(() => res(null), 7000))
// Make sure that the process instance exists in Operate.
const process = await c.getProcessInstance(p.processInstanceKey)
// If this fails, it is probably a timing issue.
// Operate is eventually consistent, so we need to wait a bit.
expect(process.key).toBe(p.processInstanceKey)
const res = await c.getJSONVariablesforProcess(p.processInstanceKey)
expect(res.foo).toBe('bar')
})
Expand Down Expand Up @@ -90,8 +108,12 @@ test('test error type', async () => {

// console.log(typeof e.response?.body)
// `string`

expect((e.response?.body as string).includes('404')).toBe(true)
if (e instanceof HTTPError) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((e as any).statusCode).toBe(404)
}
expect(e instanceof HTTPError).toBe(true)
return false
})
expect(res).toBe(false)
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/testdata/Operate-StraightThrough.bpmn
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
</bpmn:definitions>
9 changes: 8 additions & 1 deletion src/admin/lib/AdminApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
CamundaPlatform8Configuration,
DeepPartial,
GetCertificateAuthority,
GotRetryConfig,
RequireConfiguration,
constructOAuthProvider,
createUserAgentString,
gotBeforeErrorHook,
gotErrorHandler,
makeBeforeRetryHandlerFor401TokenRetry,
} from '../../lib'
import { IOAuthProvider } from '../../oauth'

Expand Down Expand Up @@ -45,13 +47,18 @@ export class AdminApiClient {
const certificateAuthority = GetCertificateAuthority(config)

this.userAgentString = createUserAgentString(config)
const prefixUrl = `${baseUrl}/clusters`
this.rest = got.extend({
prefixUrl: `${baseUrl}/clusters`,
prefixUrl,
retry: GotRetryConfig,
https: {
certificateAuthority,
},
handlers: [gotErrorHandler],
hooks: {
beforeRetry: [
makeBeforeRetryHandlerFor401TokenRetry(this.getHeaders.bind(this)),
],
beforeError: [gotBeforeErrorHook],
},
})
Expand Down
10 changes: 5 additions & 5 deletions src/c8/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { ZeebeGrpcClient } from '../zeebe'
* import { Camunda8 } from '@camunda8/sdk'
*
* const c8 = new Camunda8()
* const zeebe = c8.getZeebeGrpcClient()
* const zeebe = c8.getZeebeGrpcApiClient()
* const operate = c8.getOperateApiClient()
* const optimize = c8.getOptimizeApiClient()
* const tasklist = c8.getTasklistApiClient()
Expand All @@ -35,7 +35,7 @@ export class Camunda8 {
private modelerApiClient?: ModelerApiClient
private optimizeApiClient?: OptimizeApiClient
private tasklistApiClient?: TasklistApiClient
private zeebeGrpcClient?: ZeebeGrpcClient
private zeebeGrpcApiClient?: ZeebeGrpcClient
private configuration: CamundaPlatform8Configuration
private oAuthProvider?: OAuthProvider

Expand Down Expand Up @@ -99,12 +99,12 @@ export class Camunda8 {
}

public getZeebeGrpcApiClient(): ZeebeGrpcClient {
if (!this.zeebeGrpcClient) {
this.zeebeGrpcClient = new ZeebeGrpcClient({
if (!this.zeebeGrpcApiClient) {
this.zeebeGrpcApiClient = new ZeebeGrpcClient({
config: this.configuration,
oAuthProvider: this.oAuthProvider,
})
}
return this.zeebeGrpcClient
return this.zeebeGrpcApiClient
}
}
Loading