Skip to content
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
66 changes: 66 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
# Run all tests
npm test

# Run tests in watch mode
npm run test:watch

# Run a single test file
npx jest __tests__/client.test.ts

# Lint
npm run lint

# Lint with auto-fix
npm run lint:fix

# Build (compile TypeScript to dist/)
npx tsc

# Full pre-publish check (test + lint + build)
npm run prepublishOnly
```

## Architecture

This is a Promise-based Node.js client library for the SAP Alert Notification service for SAP BTP. It is published as `@sap_oss/alert-notification-client`.

**Entry point:** `src/index.ts` — barrel export of all public APIs. Build output goes to `dist/`.

### Public API surface (`src/client.ts`)

`AlertNotificationClient` is the main class consumers instantiate. It delegates to two sub-clients:
- `ConfigurationApiClient` (`src/configuration-api/`) — CRUD for Actions, Conditions, Subscriptions
- `EventsApiClient` (`src/producer-api/`) — sending events and pulling stored/undelivered events

### Authentication (`src/authentication.ts`)

Three strategies implementing a common `Authentication` interface:
- `BasicAuthentication` — static Base64 header
- `OAuthAuthentication` — fetches and caches tokens, handles expiry
- `CertificateAuthentication` — mTLS client certificates (JKS/P12/PFX/PEM via `src/utils/key-store.ts`)

### HTTP layer (`src/utils/axios-utils.ts`)

Axios interceptors are applied in order:
1. Authorization header injection (request)
2. Retry logic (request/response)
3. Response data extraction (response)

### Region mapping (`src/utils/region.ts`)

Maps 26+ SAP BTP region codes (e.g. `EU10`, `US20`) to their CloudFoundry, mTLS, and Mesh endpoint URLs. `RegionUtils` is the public export.

### Destination service integration (`src/utils/destination-configuration.ts`)

Optional: resolves connection details from the SAP Destination Service at runtime instead of hard-coding credentials.

## Testing

Jest with `ts-jest`. Tests live in `__tests__/`, mirroring the `src/` structure. Coverage threshold is 80% across branches, functions, lines, and statements.
29 changes: 16 additions & 13 deletions __tests__/utils/axios-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import axios, { AxiosInstance, AxiosHeaders, InternalAxiosRequestConfig } from 'axios';

import {
configureDefaultRetryInterceptor,
Expand All @@ -15,11 +15,11 @@ const mockedAuthentication = {
};

let classUnderTest: AxiosInstance;
let axiosRequestConfig: AxiosRequestConfig;
let axiosRequestConfig: InternalAxiosRequestConfig;

beforeEach(() => {
classUnderTest = axios.create();
axiosRequestConfig = {};
axiosRequestConfig = { headers: new AxiosHeaders() };
});

describe('when setupAuthorizationHeaderOnRequestInterceptor is called', () => {
Expand All @@ -35,7 +35,7 @@ describe('when setupAuthorizationHeaderOnRequestInterceptor is called', () => {
setupAuthorizationHeaderOnRequestInterceptor(classUnderTest, Promise.resolve(keyStore));

const fullfilledHandler = classUnderTest.interceptors.request['handlers'][0].fulfilled;
return fullfilledHandler(axiosRequestConfig).then((adjustedConfig) => {
return Promise.resolve(fullfilledHandler(axiosRequestConfig)).then((adjustedConfig: any) => {
expect(adjustedConfig.httpsAgent.options.pfx).toBeDefined();
expect(adjustedConfig.httpsAgent.options.passphrase).toBe('passphrase');
expect(adjustedConfig.httpsAgent.options.cert).toBeUndefined();
Expand All @@ -48,7 +48,7 @@ describe('when setupAuthorizationHeaderOnRequestInterceptor is called', () => {
setupAuthorizationHeaderOnRequestInterceptor(classUnderTest, Promise.resolve(keyStore));

const fullfilledHandler = classUnderTest.interceptors.request['handlers'][0].fulfilled;
return fullfilledHandler(axiosRequestConfig).then((adjustedConfig) => {
return Promise.resolve(fullfilledHandler(axiosRequestConfig)).then((adjustedConfig: any) => {
expect(adjustedConfig.httpsAgent.options.pfx).toBeUndefined();
expect(adjustedConfig.httpsAgent.options.passphrase).toBe('passphrase');
expect(adjustedConfig.httpsAgent.options.cert).toBeDefined();
Expand All @@ -60,10 +60,10 @@ describe('when setupAuthorizationHeaderOnRequestInterceptor is called', () => {
describe('sets up a request handler which', () => {
beforeEach(() => {
axiosRequestConfig = {
headers: {
headers: new AxiosHeaders({
'content-length': 'application-json',
'test-header': 'test-value'
}
})
};

setupAuthorizationHeaderOnRequestInterceptor(
Expand All @@ -80,15 +80,15 @@ describe('when setupAuthorizationHeaderOnRequestInterceptor is called', () => {
axiosRequestConfig = { ...axiosRequestConfig, auth };

const fullfilledHandler = classUnderTest.interceptors.request['handlers'][0].fulfilled;
return fullfilledHandler(axiosRequestConfig).then((adjustedConfig) =>
return Promise.resolve(fullfilledHandler(axiosRequestConfig)).then((adjustedConfig: any) =>
expect(adjustedConfig.auth).not.toBeDefined()
);
});

test('populates headers field with Authorization header', () => {
const fullfilledHandler = classUnderTest.interceptors.request['handlers'][0].fulfilled;

return fullfilledHandler(axiosRequestConfig).then((adjustedConfig) =>
return Promise.resolve(fullfilledHandler(axiosRequestConfig)).then((adjustedConfig: any) =>
expect(adjustedConfig.headers.Authorization).toEqual(authorizationValue)
);
});
Expand All @@ -106,12 +106,15 @@ describe('when extractDataOnResponseInterceptor is called', () => {
test: 'test-value'
},
status: 200,
statusText: 'OK',
headers: new AxiosHeaders(),
config: axiosRequestConfig,
otherObject: {
other: 'other-data'
}
};

return fullfilledHandler(responseObject).then((actualdata) =>
return Promise.resolve(fullfilledHandler(responseObject)).then((actualdata) =>
expect(actualdata).toEqual(responseObject.data)
);
});
Expand All @@ -134,7 +137,7 @@ describe('when configureDefaultRetryInterceptor is called', () => {
describe('sets up a request handler which', () => {
test('sets default retry configuration if it is not provided', () => {
const fullfilledHandler = classUnderTest.interceptors.request['handlers'][0].fulfilled;
const adjustedRequestConfig = fullfilledHandler(axiosRequestConfig);
const adjustedRequestConfig = fullfilledHandler(axiosRequestConfig) as any;

expect(adjustedRequestConfig.retryConfig).toStrictEqual({
currentAttempt: 0,
Expand All @@ -145,10 +148,10 @@ describe('when configureDefaultRetryInterceptor is called', () => {
});

test('sets the provided retry configuration', () => {
axiosRequestConfig = { ...axiosRequestConfig, ...{ retryConfig } };
axiosRequestConfig = { ...axiosRequestConfig, ...{ retryConfig } } as any;

const fullfilledHandler = classUnderTest.interceptors.request['handlers'][0].fulfilled;
const adjustedRequestConfig = fullfilledHandler(axiosRequestConfig);
const adjustedRequestConfig = fullfilledHandler(axiosRequestConfig) as any;

expect(adjustedRequestConfig.retryConfig).toStrictEqual({
...retryConfig,
Expand Down
23 changes: 12 additions & 11 deletions package-lock.json

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

Loading