diff --git a/CHANGELOG.md b/CHANGELOG.md index d1d66e91c..d9ff28c99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed an issue with the localization of the country names +- Fixed the detection of not defined values in nested objects of the portfolio details endpoint ## 3.12.0 - 2026-06-17 diff --git a/apps/api/src/helper/object.helper.spec.ts b/apps/api/src/helper/object.helper.spec.ts index ba8760c70..0ff3bcbaf 100644 --- a/apps/api/src/helper/object.helper.spec.ts +++ b/apps/api/src/helper/object.helper.spec.ts @@ -1,6 +1,10 @@ import { DEFAULT_REDACTED_PATHS } from '@ghostfolio/common/config'; -import { query, redactPaths } from './object.helper'; +import { + hasNotDefinedValuesInObject, + query, + redactPaths +} from './object.helper'; describe('query', () => { it('should get market price from stock API response', () => { @@ -22,6 +26,111 @@ describe('query', () => { }); }); +describe('hasNotDefinedValuesInObject', () => { + it('should return false when all values are defined', () => { + const object = { + currency: 'USD', + market: { + previousClose: 273.04, + price: 271.86 + }, + symbol: 'AAPL' + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(false); + }); + + it('should return true when a top-level value is null', () => { + const object = { + currency: 'USD', + marketPrice: null, + symbol: 'AAPL' + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(true); + }); + + it('should return true when a top-level value is undefined', () => { + const object = { + currency: 'USD', + marketPrice: undefined, + symbol: 'AAPL' + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(true); + }); + + it('should return true when a value nested in an object is null', () => { + const object = { + currency: 'USD', + market: { + previousClose: 273.04, + price: null + }, + symbol: 'AAPL' + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(true); + }); + + it('should return true when the first nested object is fully defined but a later sibling key is null', () => { + const object = { + market: { + previousClose: 273.04, + price: 271.86 + }, + marketPrice: null, + symbol: 'AAPL' + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(true); + }); + + it('should return true when the first nested object is fully defined but a later sibling key is undefined', () => { + const object = { + market: { + previousClose: 273.04, + price: 271.86 + }, + marketPrice: undefined, + symbol: 'AAPL' + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(true); + }); + + it('should return true when the first nested object is fully defined but a later nested object holds null', () => { + const object = { + developedMarkets: { + UNKNOWN: 0, + weight: 1 + }, + emergingMarkets: { + UNKNOWN: null, + weight: 0 + } + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(true); + }); + + it('should return false when a deeply nested object is fully defined and is followed by a defined sibling', () => { + const object = { + holding: { + market: { + region: { + UNKNOWN: 0, + developedMarkets: 1 + } + } + }, + symbol: 'AAPL' + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(false); + }); +}); + describe('redactAttributes', () => { it('should redact provided attributes', () => { expect(redactPaths({ object: {}, paths: [] })).toStrictEqual({}); diff --git a/apps/api/src/helper/object.helper.ts b/apps/api/src/helper/object.helper.ts index 350d5fe04..822ba30ec 100644 --- a/apps/api/src/helper/object.helper.ts +++ b/apps/api/src/helper/object.helper.ts @@ -7,7 +7,9 @@ export function hasNotDefinedValuesInObject(aObject: Object): boolean { if (aObject[key] === null || aObject[key] === undefined) { return true; } else if (isObject(aObject[key])) { - return hasNotDefinedValuesInObject(aObject[key]); + if (hasNotDefinedValuesInObject(aObject[key])) { + return true; + } } }