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
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { parseJSONAnnotation, parseJSONArrayAnnotation } from '../annotations';

describe('parseJSONAnnotation', () => {
it('should parse valid JSON', () => {
const annotations = { key: '["a","b"]' };
expect(parseJSONAnnotation(annotations, 'key')).toEqual(['a', 'b']);
});

it('should return defaultReturn when annotation is missing', () => {
expect(parseJSONAnnotation({}, 'missing', undefined, [])).toEqual([]);
});

it('should return defaultReturn on invalid JSON and call onError', () => {
const onError = jest.fn();
expect(parseJSONAnnotation({ key: '{bad' }, 'key', onError, 'fallback')).toBe('fallback');
expect(onError).toHaveBeenCalled();
});

it('should return undefined when annotation is missing and no default', () => {
expect(parseJSONAnnotation({}, 'missing')).toBeUndefined();
});
});

describe('parseJSONArrayAnnotation', () => {
it('should return parsed array for valid JSON array of strings', () => {
const annotations = { key: '["a","b","c"]' };
expect(parseJSONArrayAnnotation(annotations, 'key')).toEqual(['a', 'b', 'c']);
});

it('should return empty array when annotation is missing', () => {
expect(parseJSONArrayAnnotation({}, 'missing')).toEqual([]);
});

it('should return empty array when annotations is null', () => {
expect(parseJSONArrayAnnotation(null, 'key')).toEqual([]);
});

it('should return empty array for invalid JSON and call onError', () => {
const onError = jest.fn();
expect(parseJSONArrayAnnotation({ key: '{bad json' }, 'key', onError)).toEqual([]);
expect(onError).toHaveBeenCalled();
});

it('should return empty array when parsed value is a string, not an array', () => {
const onError = jest.fn();
expect(parseJSONArrayAnnotation({ key: '"just a string"' }, 'key', onError)).toEqual([]);
expect(onError).toHaveBeenCalled();
});

it('should return empty array when parsed value is an object, not an array', () => {
const onError = jest.fn();
expect(parseJSONArrayAnnotation({ key: '{"a":"b"}' }, 'key', onError)).toEqual([]);
expect(onError).toHaveBeenCalled();
});

it('should return empty array when parsed value is a number', () => {
const onError = jest.fn();
expect(parseJSONArrayAnnotation({ key: '42' }, 'key', onError)).toEqual([]);
expect(onError).toHaveBeenCalled();
});

it('should return empty array when array contains non-string elements', () => {
const onError = jest.fn();
expect(parseJSONArrayAnnotation({ key: '[1, 2, 3]' }, 'key', onError)).toEqual([]);
expect(onError).toHaveBeenCalled();
});

it('should return empty array for mixed array', () => {
const onError = jest.fn();
expect(parseJSONArrayAnnotation({ key: '["a", 1, "b"]' }, 'key', onError)).toEqual([]);
expect(onError).toHaveBeenCalled();
});

it('should return empty array for empty string annotation', () => {
expect(parseJSONArrayAnnotation({ key: '' }, 'key')).toEqual([]);
});

it('should not call onError when annotation is simply missing', () => {
const onError = jest.fn();
parseJSONArrayAnnotation({}, 'missing', onError);
expect(onError).not.toHaveBeenCalled();
});
});
24 changes: 24 additions & 0 deletions frontend/packages/console-shared/src/utils/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,27 @@ export const parseJSONAnnotation = (
return defaultReturn;
}
};

/**
* Safely parse a JSON annotation that is expected to contain an array of strings.
* Returns an empty array if the annotation is missing, malformed, or not an array of strings.
*/
export const parseJSONArrayAnnotation = (
annotations: ObjectMetadata['annotations'],
annotationKey: string,
onError?: (err: Error) => void,
): string[] => {
const parsed = parseJSONAnnotation(annotations, annotationKey, onError);
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== 'string')) {
if (parsed != null) {
const error = new Error(
`Expected annotation "${annotationKey}" to be an array of strings, got ${typeof parsed}`,
);
onError?.(error);
// eslint-disable-next-line no-console
console.warn(error.message);
}
return [];
}
return parsed;
};
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const defaultChannelFor = (packageManifest: PackageManifestKind) => {
export const defaultChannelNameFor = (pkg: PackageManifestKind) =>
pkg.status.defaultChannel || pkg?.status?.channels?.[0]?.name;
export const installModesFor = (pkg: PackageManifestKind) => (channel: string) =>
pkg.status.channels.find((ch) => ch.name === channel)?.currentCSVDesc?.installModes || [];
pkg?.status?.channels?.find((ch) => ch.name === channel)?.currentCSVDesc?.installModes ?? [];
export const supportedInstallModesFor = (pkg: PackageManifestKind) => (channel: string) =>
installModesFor(pkg)(channel).filter(({ supported }) => supported);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
import { fromRequirements } from '@console/internal/module/k8s/selector';
import { isCatalogTypeEnabled, useIsDeveloperCatalogEnabled } from '@console/shared';
import { ErrorBoundaryFallbackPage, withFallback } from '@console/shared/src/components/error';
import { parseJSONAnnotation } from '@console/shared/src/utils/annotations';
import { parseJSONArrayAnnotation } from '@console/shared/src/utils/annotations';
import { iconFor } from '..';
import {
CloudCredentialModel,
Expand Down Expand Up @@ -119,10 +119,10 @@ export const OperatorHubList: React.FC<OperatorHubListProps> = ({
currentCSVDesc?.annotations ?? {};
const [parsedInfraFeatures, validSubscription] = ANNOTATIONS_WITH_JSON.map(
(annotationKey) =>
parseJSONAnnotation(currentCSVAnnotations, annotationKey, () =>
parseJSONArrayAnnotation(currentCSVAnnotations, annotationKey, () =>
// eslint-disable-next-line no-console
console.warn(`Error parsing annotation in PackageManifest ${pkg.metadata.name}`),
) ?? [],
),
);

const validSubscriptionFilters = validSubscription.reduce(
Expand Down
6 changes: 3 additions & 3 deletions frontend/packages/operator-lifecycle-manager/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { ObjectMetadata } from '@console/internal/module/k8s';
import { parseJSONAnnotation } from '@console/shared/src/utils/annotations';
import { parseJSONArrayAnnotation } from '@console/shared/src/utils/annotations';
import { INTERNAL_OBJECTS_ANNOTATION, OPERATOR_PLUGINS_ANNOTATION } from './const';
import { SubscriptionKind, SubscriptionState } from './types';

export const getClusterServiceVersionPlugins = (
annotations: ObjectMetadata['annotations'],
): string[] => parseJSONAnnotation(annotations, OPERATOR_PLUGINS_ANNOTATION) ?? [];
): string[] => parseJSONArrayAnnotation(annotations, OPERATOR_PLUGINS_ANNOTATION);

export const getInternalObjects = (annotations: ObjectMetadata['annotations']): string[] =>
parseJSONAnnotation(annotations, INTERNAL_OBJECTS_ANNOTATION) ?? [];
parseJSONArrayAnnotation(annotations, INTERNAL_OBJECTS_ANNOTATION);

export const isCatalogSourceTrusted = (catalogSource: string): boolean =>
catalogSource === 'redhat-operators';
Expand Down