From 9ee0e5ee3ec8ac93c7ef10212723faaa25d453d6 Mon Sep 17 00:00:00 2001
From: Robert Duda <robert.duda@hyland.com>
Date: Wed, 30 Apr 2025 16:59:50 +0200
Subject: [PATCH] AAE-19688 Persist feature flag overrides in session storage
 (#10832)

* AAE-19688 Persist feature flag overrides in session storage

* unit tests
---
 .../services/debug-features.service.spec.ts   | 32 +++++++++---------
 .../lib/services/debug-features.service.ts    | 33 ++++++++++++-------
 .../services/storage-features.service.spec.ts | 29 ++++++++--------
 .../lib/services/storage-features.service.ts  | 29 +++++++++-------
 4 files changed, 71 insertions(+), 52 deletions(-)

diff --git a/lib/core/feature-flags/src/lib/services/debug-features.service.spec.ts b/lib/core/feature-flags/src/lib/services/debug-features.service.spec.ts
index 550adff8d8..f6b0f9dc09 100644
--- a/lib/core/feature-flags/src/lib/services/debug-features.service.spec.ts
+++ b/lib/core/feature-flags/src/lib/services/debug-features.service.spec.ts
@@ -17,37 +17,37 @@
 
 import { TestBed } from '@angular/core/testing';
 import { DebugFeaturesService } from './debug-features.service';
-import { StorageService } from '../../../../src/lib/common/services/storage.service';
-import { OverridableFeaturesServiceToken, WritableFeaturesServiceToken } from '../interfaces/features.interface';
+import { OverridableFeaturesServiceToken, WritableFeaturesServiceConfigToken, WritableFeaturesServiceToken } from '../interfaces/features.interface';
 import { DummyFeaturesService } from './dummy-features.service';
 import { StorageFeaturesService } from './storage-features.service';
 import { take } from 'rxjs/operators';
 
 describe('DebugFeaturesService', () => {
     let service: DebugFeaturesService;
-    const mockStorage = {
-        getItem: () =>
-            JSON.stringify({
-                feature1: {
-                    current: true
-                },
-                feature2: {
-                    current: false,
-                    fictive: true
-                }
-            }),
-        setItem: () => {}
-    };
+    let mockStorageKey: string;
+    let mockStorage;
 
     beforeEach(() => {
+        mockStorageKey = 'storage-key-test';
+        mockStorage = { [mockStorageKey]: true };
+
         TestBed.configureTestingModule({
             providers: [
                 DebugFeaturesService,
-                { provide: StorageService, useValue: mockStorage },
+                {
+                    provide: WritableFeaturesServiceConfigToken,
+                    useValue: { storageKey: mockStorageKey }
+                },
                 { provide: WritableFeaturesServiceToken, useClass: StorageFeaturesService },
                 { provide: OverridableFeaturesServiceToken, useClass: DummyFeaturesService }
             ]
         });
+
+        spyOn(sessionStorage, 'getItem').and.callFake((key) => JSON.stringify(mockStorage[key]));
+        spyOn(sessionStorage, 'setItem').and.callFake((key, value) => {
+            mockStorage[key] = value;
+        });
+
         service = TestBed.inject(DebugFeaturesService);
     });
 
diff --git a/lib/core/feature-flags/src/lib/services/debug-features.service.ts b/lib/core/feature-flags/src/lib/services/debug-features.service.ts
index bacf205a48..0a6434b5b8 100644
--- a/lib/core/feature-flags/src/lib/services/debug-features.service.ts
+++ b/lib/core/feature-flags/src/lib/services/debug-features.service.ts
@@ -16,8 +16,8 @@
  */
 
 import { Inject, Injectable, Optional } from '@angular/core';
-import { BehaviorSubject, Observable } from 'rxjs';
-import { skip, switchMap } from 'rxjs/operators';
+import { BehaviorSubject, Observable, combineLatest } from 'rxjs';
+import { filter, switchMap } from 'rxjs/operators';
 import {
     IDebugFeaturesService,
     IFeaturesService,
@@ -29,12 +29,12 @@ import {
     FlagSet,
     IWritableFeaturesService
 } from '../interfaces/features.interface';
-import { StorageService } from '@alfresco/adf-core';
 
 @Injectable()
 export class DebugFeaturesService implements IDebugFeaturesService {
-    private isInDebugMode: BehaviorSubject<boolean>;
-    private isInDebugMode$: Observable<boolean>;
+    private readonly isInDebugModeSubject = new BehaviorSubject<boolean>(false);
+    private readonly isInDebugMode$ = this.isInDebugModeSubject.asObservable();
+    private readonly initSubject = new BehaviorSubject<boolean>(false);
 
     get storageKey(): string {
         return `${this.config?.storageKey || 'feature-flags'}-override`;
@@ -43,14 +43,15 @@ export class DebugFeaturesService implements IDebugFeaturesService {
     constructor(
         @Inject(OverridableFeaturesServiceToken) private overriddenFeaturesService: IFeaturesService,
         @Inject(WritableFeaturesServiceToken) private writableFeaturesService: IFeaturesService & IWritableFeaturesService,
-        private storageService: StorageService,
         @Optional() @Inject(WritableFeaturesServiceConfigToken) private config?: WritableFeaturesServiceConfig
     ) {
-        this.isInDebugMode = new BehaviorSubject<boolean>(JSON.parse(this.storageService.getItem(this.storageKey) || 'false'));
-        this.isInDebugMode$ = this.isInDebugMode.asObservable();
+        this.init();
 
-        this.isInDebugMode.pipe(skip(1)).subscribe((debugMode) => {
-            this.storageService.setItem(this.storageKey, JSON.stringify(debugMode));
+        combineLatest({
+            debugMode: this.isInDebugModeSubject,
+            init: this.waitForInitializationToFinish()
+        }).subscribe(({ debugMode }) => {
+            sessionStorage.setItem(this.storageKey, JSON.stringify(debugMode));
         });
     }
 
@@ -87,10 +88,20 @@ export class DebugFeaturesService implements IDebugFeaturesService {
     }
 
     enable(on: boolean): void {
-        this.isInDebugMode.next(on);
+        this.isInDebugModeSubject.next(on);
     }
 
     isEnabled$(): Observable<boolean> {
         return this.isInDebugMode$;
     }
+
+    private init() {
+        const storedOverride = JSON.parse(sessionStorage.getItem(this.storageKey) || 'false');
+        this.isInDebugModeSubject.next(storedOverride);
+        this.initSubject.next(true);
+    }
+
+    private waitForInitializationToFinish(): Observable<boolean> {
+        return this.initSubject.pipe(filter((initialized) => !!initialized));
+    }
 }
diff --git a/lib/core/feature-flags/src/lib/services/storage-features.service.spec.ts b/lib/core/feature-flags/src/lib/services/storage-features.service.spec.ts
index 10451e3459..054f49edf8 100644
--- a/lib/core/feature-flags/src/lib/services/storage-features.service.spec.ts
+++ b/lib/core/feature-flags/src/lib/services/storage-features.service.spec.ts
@@ -17,17 +17,20 @@
 
 import { TestBed } from '@angular/core/testing';
 import { StorageFeaturesService } from './storage-features.service';
-import { StorageService } from '../../../../src/public-api';
 import { FlagSet, WritableFeaturesServiceConfigToken } from '../interfaces/features.interface';
 import { skip, take } from 'rxjs/operators';
 
 describe('StorageFeaturesService', () => {
     let storageFeaturesService: StorageFeaturesService;
 
-    describe('if flags are present in LocalStorage', () => {
-        const mockStorage = {
-            getItem: () =>
-                JSON.stringify({
+    describe('if flags are present in sessionStorage', () => {
+        let mockStorageKey: string;
+        let mockStorage;
+
+        beforeEach(() => {
+            mockStorageKey = 'storage-key-test';
+            mockStorage = {
+                [mockStorageKey]: {
                     feature1: {
                         current: true
                     },
@@ -35,23 +38,23 @@ describe('StorageFeaturesService', () => {
                         current: false,
                         fictive: true
                     }
-                }),
-            setItem: () => {}
-        };
+                }
+            };
 
-        beforeEach(() => {
             TestBed.configureTestingModule({
                 providers: [
-                    { provide: StorageService, useValue: mockStorage },
                     {
                         provide: WritableFeaturesServiceConfigToken,
-                        useValue: {
-                            storageKey: 'storage-key-test'
-                        }
+                        useValue: { storageKey: mockStorageKey }
                     }
                 ]
             });
 
+            spyOn(sessionStorage, 'getItem').and.callFake((key) => JSON.stringify(mockStorage[key]));
+            spyOn(sessionStorage, 'setItem').and.callFake((key, value) => {
+                mockStorage[key] = value;
+            });
+
             storageFeaturesService = TestBed.inject(StorageFeaturesService);
             storageFeaturesService.init();
         });
diff --git a/lib/core/feature-flags/src/lib/services/storage-features.service.ts b/lib/core/feature-flags/src/lib/services/storage-features.service.ts
index 56c2b414de..6dc9b07afe 100644
--- a/lib/core/feature-flags/src/lib/services/storage-features.service.ts
+++ b/lib/core/feature-flags/src/lib/services/storage-features.service.ts
@@ -16,8 +16,8 @@
  */
 
 import { Inject, Injectable, Optional } from '@angular/core';
-import { BehaviorSubject, Observable, of } from 'rxjs';
-import { map, skip } from 'rxjs/operators';
+import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
+import { filter, map } from 'rxjs/operators';
 import {
     FlagChangeset,
     IFeaturesService,
@@ -28,21 +28,21 @@ import {
     WritableFeaturesServiceConfig
 } from '../interfaces/features.interface';
 import { FlagSetParser } from './flagset.parser';
-import { StorageService } from '@alfresco/adf-core';
 
 @Injectable({ providedIn: 'root' })
 export class StorageFeaturesService implements IFeaturesService, IWritableFeaturesService {
     private currentFlagState: WritableFlagChangeset = {};
-    private flags = new BehaviorSubject<WritableFlagChangeset>({});
-    private flags$ = this.flags.asObservable();
+    private readonly flags = new BehaviorSubject<WritableFlagChangeset>({});
+    private readonly flags$ = this.flags.asObservable();
+    private readonly initSubject = new BehaviorSubject<boolean>(false);
 
-    constructor(
-        private storageService: StorageService,
-        @Optional() @Inject(WritableFeaturesServiceConfigToken) private config?: WritableFeaturesServiceConfig
-    ) {
-        this.flags.pipe(skip(1)).subscribe((flags) => {
+    constructor(@Optional() @Inject(WritableFeaturesServiceConfigToken) private config?: WritableFeaturesServiceConfig) {
+        combineLatest({
+            flags: this.flags,
+            init: this.waitForInitializationToFinish()
+        }).subscribe(({ flags }) => {
             this.currentFlagState = flags;
-            this.storageService.setItem(this.storageKey, JSON.stringify(FlagSetParser.serialize(flags)));
+            sessionStorage.setItem(this.storageKey, JSON.stringify(FlagSetParser.serialize(flags)));
         });
     }
 
@@ -51,9 +51,10 @@ export class StorageFeaturesService implements IFeaturesService, IWritableFeatur
     }
 
     init(): Observable<WritableFlagChangeset> {
-        const storedFlags = JSON.parse(this.storageService.getItem(this.storageKey) || '{}');
+        const storedFlags = JSON.parse(sessionStorage.getItem(this.storageKey) || '{}');
         const initialFlagChangeSet = FlagSetParser.deserialize(storedFlags);
         this.flags.next(initialFlagChangeSet);
+        this.initSubject.next(true);
         return of(initialFlagChangeSet);
     }
 
@@ -133,4 +134,8 @@ export class StorageFeaturesService implements IFeaturesService, IWritableFeatur
 
         this.flags.next(mergedFlags);
     }
+
+    private waitForInitializationToFinish(): Observable<boolean> {
+        return this.initSubject.pipe(filter((initialized) => !!initialized));
+    }
 }