Skip to content

Commit dd46233

Browse files
shahzad31cursoragent
authored andcommitted
[Observability] Replace basic-license annotations FTR test with unit coverage !! (elastic#270174)
## Summary The `observability_api_integration/basic` FTR config existed solely to assert that `POST /api/observability/annotation` returns `403` on a basic license. That single behavior is now covered directly at `create_annotations_client.ts`'s `ensureGoldLicense` guard via unit tests, removing the need for a dedicated FTR config (and the associated basic-license ES boot) just to exercise one branch. Part of elastic#263519. ### Changes - **Added** `x-pack/solutions/observability/plugins/observability/server/lib/annotations/create_annotations_client.test.ts` covering: - The `ensureGoldLicense` guard for every gated method (`create`, `update`, `getById`, `find`, `delete`) with both an absent license and a license below gold. Each asserts the exact `Boom.forbidden('Annotations require at least a gold license or a trial license.')` shape the route handler relies on. - The `permissions` endpoint's `hasGoldLicense` reporting under absent / below-gold / gold-or-higher licenses. - **Removed** `x-pack/solutions/observability/test/observability_api_integration/basic/` (config + `tests/index.ts` + `tests/annotations.ts`). - **Removed** the entry from `.buildkite/ftr-manifests/ftr_oblt_stateful_configs.yml`. - **Updated** the plugin README to reflect the new layout (the basic-license section is gone; trial remains). ### Why unit tests instead of a Scout port The FTR test was an integration test for a one-line license check. Reproducing it on Scout would require either a brand-new `kbn-scout` server config set that boots ES with `license: 'basic'` (just to flip a single line of behavior), or runtime license mutation against a shared Scout server. Both are heavier than the coverage they buy. Unit coverage at the client level exercises the same guard and is also net-new — there were no existing unit tests for this module. ## Test plan - [x] `node scripts/jest x-pack/solutions/observability/plugins/observability/server/lib/annotations/create_annotations_client.test.ts` — 14/14 passing locally. - [x] `node scripts/eslint --fix x-pack/solutions/observability/plugins/observability/server/lib/annotations/create_annotations_client.test.ts` — clean. - [ ] `node scripts/type_check --project x-pack/solutions/observability/plugins/observability/tsconfig.json` — running locally; will confirm in CI as well. - [ ] CI green. Made with [Cursor](https://cursor.com) Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2a780a2 commit dd46233

6 files changed

Lines changed: 184 additions & 96 deletions

File tree

.buildkite/ftr-manifests/ftr_oblt_stateful_configs.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ enabled:
2828
- x-pack/solutions/observability/test/apm_api_integration/rules/config.ts
2929
- x-pack/solutions/observability/test/apm_api_integration/trial/config.ts
3030
- x-pack/solutions/observability/test/functional/apps/dataset_quality/config.ts
31-
- x-pack/solutions/observability/test/observability_api_integration/basic/config.ts
3231
- x-pack/solutions/observability/test/observability_api_integration/trial/config.ts
3332
- x-pack/solutions/observability/test/observability_functional/with_rac_write.config.ts
3433
- x-pack/solutions/observability/test/observability_ai_assistant_functional/enterprise/config.ts

x-pack/solutions/observability/plugins/observability/README.md

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,7 @@ open target/coverage/jest/index.html
4747

4848
## API integration testing
4949

50-
API tests are separated in two suites:
51-
52-
- a basic license test suite
53-
- a trial license test suite (the equivalent of gold+)
54-
55-
This requires separate test servers and test runners.
56-
57-
### Basic
58-
59-
```
60-
# Start server
61-
node scripts/functional_tests_server --config x-pack/solutions/observability/test/observability_api_integration/basic/config.ts
62-
63-
# Run tests
64-
node scripts/functional_test_runner --config x-pack/solutions/observability/test/observability_api_integration/basic/config.ts
65-
```
66-
67-
The API tests for "basic" are located in `x-pack/solutions/observability/test/observability_api_integration/basic/tests`.
50+
API tests run under a trial license (the equivalent of gold+). Basic-license behavior is covered by unit tests (see `server/lib/annotations/create_annotations_client.test.ts`).
6851

6952
### Trial
7053

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
9+
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
10+
import { loggerMock } from '@kbn/logging-mocks';
11+
import { DEFAULT_ANNOTATION_INDEX } from '../../../common/annotations';
12+
import { createAnnotationsClient } from './create_annotations_client';
13+
14+
const FORBIDDEN_MESSAGE = 'Annotations require at least a gold license or a trial license.';
15+
16+
const baseAnnotation = {
17+
annotation: { type: 'deployment' },
18+
'@timestamp': '2026-05-20T00:00:00.000Z',
19+
message: 'test message',
20+
tags: ['apm'],
21+
};
22+
23+
const buildClient = ({
24+
license,
25+
}: {
26+
license?: ReturnType<typeof licensingMock.createLicenseMock>;
27+
} = {}) => {
28+
const esClient = elasticsearchServiceMock.createElasticsearchClient();
29+
const logger = loggerMock.create();
30+
31+
return {
32+
esClient,
33+
client: createAnnotationsClient({
34+
index: DEFAULT_ANNOTATION_INDEX,
35+
esClient,
36+
logger,
37+
license,
38+
}),
39+
};
40+
};
41+
42+
const mockHasPrivileges = (
43+
esClient: ReturnType<typeof elasticsearchServiceMock.createElasticsearchClient>
44+
) => {
45+
esClient.security.hasPrivileges.mockResponse({
46+
username: 'elastic',
47+
has_all_requested: true,
48+
cluster: {},
49+
index: {
50+
[DEFAULT_ANNOTATION_INDEX]: { read: true, write: true },
51+
},
52+
application: {},
53+
});
54+
};
55+
56+
describe('createAnnotationsClient', () => {
57+
describe('license guard', () => {
58+
// The license guard throws synchronously before the wrapped async function runs,
59+
// so we await an already-resolved promise then invoke the call inside the .then,
60+
// turning the sync throw into a rejection we can inspect.
61+
const captureError = async (fn: () => unknown) => {
62+
try {
63+
await Promise.resolve().then(fn);
64+
} catch (error) {
65+
return error;
66+
}
67+
throw new Error('Expected the call to throw a forbidden error');
68+
};
69+
70+
const expectForbidden = (error: any) => {
71+
expect(error).toBeDefined();
72+
expect(error.isBoom).toBe(true);
73+
expect(error.output.statusCode).toBe(403);
74+
expect(error.output.payload).toEqual({
75+
statusCode: 403,
76+
error: 'Forbidden',
77+
message: FORBIDDEN_MESSAGE,
78+
});
79+
};
80+
81+
it.each(['create', 'update', 'getById', 'find', 'delete'] as const)(
82+
'rejects %s with a forbidden error when the license is missing',
83+
async (method) => {
84+
const { client, esClient } = buildClient();
85+
86+
const error = await captureError(() =>
87+
(client[method] as (params: unknown) => Promise<unknown>)({})
88+
);
89+
expectForbidden(error);
90+
91+
expect(esClient.index).not.toHaveBeenCalled();
92+
expect(esClient.search).not.toHaveBeenCalled();
93+
expect(esClient.deleteByQuery).not.toHaveBeenCalled();
94+
}
95+
);
96+
97+
it.each(['create', 'update', 'getById', 'find', 'delete'] as const)(
98+
'rejects %s with a forbidden error when the license is below gold',
99+
async (method) => {
100+
const license = licensingMock.createLicenseMock();
101+
license.hasAtLeast.mockReturnValue(false);
102+
103+
const { client } = buildClient({ license });
104+
105+
const error = await captureError(() =>
106+
(client[method] as (params: unknown) => Promise<unknown>)({})
107+
);
108+
expectForbidden(error);
109+
110+
expect(license.hasAtLeast).toHaveBeenCalledWith('gold');
111+
}
112+
);
113+
114+
it('lets create proceed past the license guard when the license is gold or higher', async () => {
115+
const license = licensingMock.createLicenseMock();
116+
license.hasAtLeast.mockReturnValue(true);
117+
118+
const { client, esClient } = buildClient({ license });
119+
120+
esClient.index.mockResponse({
121+
_id: 'annotation-1',
122+
_index: DEFAULT_ANNOTATION_INDEX,
123+
_shards: { total: 1, successful: 1, failed: 0 },
124+
result: 'created',
125+
_version: 1,
126+
_seq_no: 0,
127+
_primary_term: 1,
128+
});
129+
130+
await expect(client.create(baseAnnotation)).resolves.toEqual({
131+
_id: 'annotation-1',
132+
_index: DEFAULT_ANNOTATION_INDEX,
133+
_source: expect.objectContaining({
134+
message: 'test message',
135+
annotation: expect.objectContaining({ type: 'deployment', title: 'test message' }),
136+
}),
137+
});
138+
139+
expect(license.hasAtLeast).toHaveBeenCalledWith('gold');
140+
expect(esClient.index).toHaveBeenCalledTimes(1);
141+
});
142+
});
143+
144+
describe('permissions', () => {
145+
it('reports hasGoldLicense: false when the license is below gold', async () => {
146+
const license = licensingMock.createLicenseMock();
147+
license.hasAtLeast.mockReturnValue(false);
148+
149+
const { client, esClient } = buildClient({ license });
150+
mockHasPrivileges(esClient);
151+
152+
const permissions = await client.permissions();
153+
154+
expect(permissions).toEqual({
155+
index: DEFAULT_ANNOTATION_INDEX,
156+
hasGoldLicense: false,
157+
read: true,
158+
write: true,
159+
});
160+
});
161+
162+
it('reports hasGoldLicense: false when no license is provided', async () => {
163+
const { client, esClient } = buildClient();
164+
mockHasPrivileges(esClient);
165+
166+
const permissions = await client.permissions();
167+
168+
expect(permissions.hasGoldLicense).toBe(false);
169+
});
170+
171+
it('reports hasGoldLicense: true when the license is gold or higher', async () => {
172+
const license = licensingMock.createLicenseMock();
173+
license.hasAtLeast.mockReturnValue(true);
174+
175+
const { client, esClient } = buildClient({ license });
176+
mockHasPrivileges(esClient);
177+
178+
const permissions = await client.permissions();
179+
180+
expect(permissions.hasGoldLicense).toBe(true);
181+
});
182+
});
183+
});

x-pack/solutions/observability/test/observability_api_integration/basic/config.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

x-pack/solutions/observability/test/observability_api_integration/basic/tests/annotations.ts

Lines changed: 0 additions & 48 deletions
This file was deleted.

x-pack/solutions/observability/test/observability_api_integration/basic/tests/index.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)