AAE-19688 Persist feature flag overrides in session storage (#10832)

* AAE-19688 Persist feature flag overrides in session storage

* unit tests
This commit is contained in:
Robert Duda 2025-04-30 16:59:50 +02:00 committed by GitHub
parent 61a1fb64bf
commit 9ee0e5ee3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 71 additions and 52 deletions

View File

@ -17,37 +17,37 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { DebugFeaturesService } from './debug-features.service'; import { DebugFeaturesService } from './debug-features.service';
import { StorageService } from '../../../../src/lib/common/services/storage.service'; import { OverridableFeaturesServiceToken, WritableFeaturesServiceConfigToken, WritableFeaturesServiceToken } from '../interfaces/features.interface';
import { OverridableFeaturesServiceToken, WritableFeaturesServiceToken } from '../interfaces/features.interface';
import { DummyFeaturesService } from './dummy-features.service'; import { DummyFeaturesService } from './dummy-features.service';
import { StorageFeaturesService } from './storage-features.service'; import { StorageFeaturesService } from './storage-features.service';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
describe('DebugFeaturesService', () => { describe('DebugFeaturesService', () => {
let service: DebugFeaturesService; let service: DebugFeaturesService;
const mockStorage = { let mockStorageKey: string;
getItem: () => let mockStorage;
JSON.stringify({
feature1: {
current: true
},
feature2: {
current: false,
fictive: true
}
}),
setItem: () => {}
};
beforeEach(() => { beforeEach(() => {
mockStorageKey = 'storage-key-test';
mockStorage = { [mockStorageKey]: true };
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
DebugFeaturesService, DebugFeaturesService,
{ provide: StorageService, useValue: mockStorage }, {
provide: WritableFeaturesServiceConfigToken,
useValue: { storageKey: mockStorageKey }
},
{ provide: WritableFeaturesServiceToken, useClass: StorageFeaturesService }, { provide: WritableFeaturesServiceToken, useClass: StorageFeaturesService },
{ provide: OverridableFeaturesServiceToken, useClass: DummyFeaturesService } { 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); service = TestBed.inject(DebugFeaturesService);
}); });

View File

@ -16,8 +16,8 @@
*/ */
import { Inject, Injectable, Optional } from '@angular/core'; import { Inject, Injectable, Optional } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable, combineLatest } from 'rxjs';
import { skip, switchMap } from 'rxjs/operators'; import { filter, switchMap } from 'rxjs/operators';
import { import {
IDebugFeaturesService, IDebugFeaturesService,
IFeaturesService, IFeaturesService,
@ -29,12 +29,12 @@ import {
FlagSet, FlagSet,
IWritableFeaturesService IWritableFeaturesService
} from '../interfaces/features.interface'; } from '../interfaces/features.interface';
import { StorageService } from '@alfresco/adf-core';
@Injectable() @Injectable()
export class DebugFeaturesService implements IDebugFeaturesService { export class DebugFeaturesService implements IDebugFeaturesService {
private isInDebugMode: BehaviorSubject<boolean>; private readonly isInDebugModeSubject = new BehaviorSubject<boolean>(false);
private isInDebugMode$: Observable<boolean>; private readonly isInDebugMode$ = this.isInDebugModeSubject.asObservable();
private readonly initSubject = new BehaviorSubject<boolean>(false);
get storageKey(): string { get storageKey(): string {
return `${this.config?.storageKey || 'feature-flags'}-override`; return `${this.config?.storageKey || 'feature-flags'}-override`;
@ -43,14 +43,15 @@ export class DebugFeaturesService implements IDebugFeaturesService {
constructor( constructor(
@Inject(OverridableFeaturesServiceToken) private overriddenFeaturesService: IFeaturesService, @Inject(OverridableFeaturesServiceToken) private overriddenFeaturesService: IFeaturesService,
@Inject(WritableFeaturesServiceToken) private writableFeaturesService: IFeaturesService & IWritableFeaturesService, @Inject(WritableFeaturesServiceToken) private writableFeaturesService: IFeaturesService & IWritableFeaturesService,
private storageService: StorageService,
@Optional() @Inject(WritableFeaturesServiceConfigToken) private config?: WritableFeaturesServiceConfig @Optional() @Inject(WritableFeaturesServiceConfigToken) private config?: WritableFeaturesServiceConfig
) { ) {
this.isInDebugMode = new BehaviorSubject<boolean>(JSON.parse(this.storageService.getItem(this.storageKey) || 'false')); this.init();
this.isInDebugMode$ = this.isInDebugMode.asObservable();
this.isInDebugMode.pipe(skip(1)).subscribe((debugMode) => { combineLatest({
this.storageService.setItem(this.storageKey, JSON.stringify(debugMode)); 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 { enable(on: boolean): void {
this.isInDebugMode.next(on); this.isInDebugModeSubject.next(on);
} }
isEnabled$(): Observable<boolean> { isEnabled$(): Observable<boolean> {
return this.isInDebugMode$; 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));
}
} }

View File

@ -17,17 +17,20 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { StorageFeaturesService } from './storage-features.service'; import { StorageFeaturesService } from './storage-features.service';
import { StorageService } from '../../../../src/public-api';
import { FlagSet, WritableFeaturesServiceConfigToken } from '../interfaces/features.interface'; import { FlagSet, WritableFeaturesServiceConfigToken } from '../interfaces/features.interface';
import { skip, take } from 'rxjs/operators'; import { skip, take } from 'rxjs/operators';
describe('StorageFeaturesService', () => { describe('StorageFeaturesService', () => {
let storageFeaturesService: StorageFeaturesService; let storageFeaturesService: StorageFeaturesService;
describe('if flags are present in LocalStorage', () => { describe('if flags are present in sessionStorage', () => {
const mockStorage = { let mockStorageKey: string;
getItem: () => let mockStorage;
JSON.stringify({
beforeEach(() => {
mockStorageKey = 'storage-key-test';
mockStorage = {
[mockStorageKey]: {
feature1: { feature1: {
current: true current: true
}, },
@ -35,23 +38,23 @@ describe('StorageFeaturesService', () => {
current: false, current: false,
fictive: true fictive: true
} }
}), }
setItem: () => {} };
};
beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
{ provide: StorageService, useValue: mockStorage },
{ {
provide: WritableFeaturesServiceConfigToken, provide: WritableFeaturesServiceConfigToken,
useValue: { useValue: { storageKey: mockStorageKey }
storageKey: 'storage-key-test'
}
} }
] ]
}); });
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 = TestBed.inject(StorageFeaturesService);
storageFeaturesService.init(); storageFeaturesService.init();
}); });

View File

@ -16,8 +16,8 @@
*/ */
import { Inject, Injectable, Optional } from '@angular/core'; import { Inject, Injectable, Optional } from '@angular/core';
import { BehaviorSubject, Observable, of } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import { map, skip } from 'rxjs/operators'; import { filter, map } from 'rxjs/operators';
import { import {
FlagChangeset, FlagChangeset,
IFeaturesService, IFeaturesService,
@ -28,21 +28,21 @@ import {
WritableFeaturesServiceConfig WritableFeaturesServiceConfig
} from '../interfaces/features.interface'; } from '../interfaces/features.interface';
import { FlagSetParser } from './flagset.parser'; import { FlagSetParser } from './flagset.parser';
import { StorageService } from '@alfresco/adf-core';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class StorageFeaturesService implements IFeaturesService, IWritableFeaturesService { export class StorageFeaturesService implements IFeaturesService, IWritableFeaturesService {
private currentFlagState: WritableFlagChangeset = {}; private currentFlagState: WritableFlagChangeset = {};
private flags = new BehaviorSubject<WritableFlagChangeset>({}); private readonly flags = new BehaviorSubject<WritableFlagChangeset>({});
private flags$ = this.flags.asObservable(); private readonly flags$ = this.flags.asObservable();
private readonly initSubject = new BehaviorSubject<boolean>(false);
constructor( constructor(@Optional() @Inject(WritableFeaturesServiceConfigToken) private config?: WritableFeaturesServiceConfig) {
private storageService: StorageService, combineLatest({
@Optional() @Inject(WritableFeaturesServiceConfigToken) private config?: WritableFeaturesServiceConfig flags: this.flags,
) { init: this.waitForInitializationToFinish()
this.flags.pipe(skip(1)).subscribe((flags) => { }).subscribe(({ flags }) => {
this.currentFlagState = 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> { 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); const initialFlagChangeSet = FlagSetParser.deserialize(storedFlags);
this.flags.next(initialFlagChangeSet); this.flags.next(initialFlagChangeSet);
this.initSubject.next(true);
return of(initialFlagChangeSet); return of(initialFlagChangeSet);
} }
@ -133,4 +134,8 @@ export class StorageFeaturesService implements IFeaturesService, IWritableFeatur
this.flags.next(mergedFlags); this.flags.next(mergedFlags);
} }
private waitForInitializationToFinish(): Observable<boolean> {
return this.initSubject.pipe(filter((initialized) => !!initialized));
}
} }