Skip to content

Commit 252fbd4

Browse files
[FSSDK-12647] user id vuid adjustment
1 parent 78d9782 commit 252fbd4

6 files changed

Lines changed: 270 additions & 24 deletions

File tree

src/provider/OptimizelyProvider.spec.tsx

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ describe('OptimizelyProvider', () => {
233233
let capturedContext: OptimizelyContextValue | null = null;
234234

235235
const { unmount } = render(
236-
<OptimizelyProvider client={mockClient}>
236+
<OptimizelyProvider client={mockClient} user={{ id: 'user-1' }}>
237237
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
238238
</OptimizelyProvider>
239239
);
@@ -488,7 +488,7 @@ describe('OptimizelyProvider', () => {
488488
expect(mockClient.createUserContext).toHaveBeenCalledTimes(1);
489489
});
490490

491-
it('should create user context without userId when user prop is not provided', async () => {
491+
it('should not create user context when user prop is not provided', async () => {
492492
const mockClient = createMockClient();
493493

494494
render(
@@ -497,7 +497,7 @@ describe('OptimizelyProvider', () => {
497497
</OptimizelyProvider>
498498
);
499499

500-
expect(mockClient.createUserContext).toHaveBeenCalledWith(undefined, undefined);
500+
expect(mockClient.createUserContext).not.toHaveBeenCalled();
501501
});
502502
});
503503

@@ -793,6 +793,56 @@ describe('OptimizelyProvider', () => {
793793
});
794794
});
795795

796+
describe('null user', () => {
797+
it('should not create user context when user is null', async () => {
798+
const mockClient = createMockClient();
799+
800+
render(
801+
<OptimizelyProvider client={mockClient} user={null}>
802+
<div>Child</div>
803+
</OptimizelyProvider>
804+
);
805+
806+
expect(mockClient.createUserContext).not.toHaveBeenCalled();
807+
});
808+
809+
it('should have null userContext in store when user is null', async () => {
810+
const mockClient = createMockClient();
811+
let capturedContext: OptimizelyContextValue | null = null;
812+
813+
render(
814+
<OptimizelyProvider client={mockClient} user={null}>
815+
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
816+
</OptimizelyProvider>
817+
);
818+
819+
expect(capturedContext).not.toBeNull();
820+
expect(capturedContext!.store.getState().userContext).toBeNull();
821+
});
822+
823+
it('should create context when user changes from null to valid', async () => {
824+
const mockClient = createMockClient();
825+
let capturedContext: OptimizelyContextValue | null = null;
826+
827+
const { rerender } = render(
828+
<OptimizelyProvider client={mockClient} user={null}>
829+
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
830+
</OptimizelyProvider>
831+
);
832+
833+
expect(mockClient.createUserContext).not.toHaveBeenCalled();
834+
expect(capturedContext!.store.getState().userContext).toBeNull();
835+
836+
rerender(
837+
<OptimizelyProvider client={mockClient} user={{ id: 'user-1' }}>
838+
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
839+
</OptimizelyProvider>
840+
);
841+
842+
expect(mockClient.createUserContext).toHaveBeenCalledWith('user-1', undefined);
843+
});
844+
});
845+
796846
describe('context reference identity', () => {
797847
it('should change context value reference when client changes', async () => {
798848
const mockClient1 = createMockClient();

src/provider/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export interface OptimizelyProviderProps {
3838
/**
3939
* User information for decisions.
4040
*/
41-
user?: UserInfo;
41+
user?: UserInfo | null;
4242

4343
/**
4444
* Timeout in milliseconds to wait for the client to become ready.

src/utils/UserContextManager.spec.ts

Lines changed: 176 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ describe('UserContextManager', () => {
126126
const config = createManagerConfig(client);
127127
const manager = new UserContextManager(config);
128128

129-
manager.resolveUserContext(); // no user
129+
manager.resolveUserContext({}); // empty object = VUID mode
130130
await flushPromises();
131131

132132
// Should be waiting on onReady
@@ -158,7 +158,7 @@ describe('UserContextManager', () => {
158158
const config = createManagerConfig(client);
159159
const manager = new UserContextManager(config);
160160

161-
manager.resolveUserContext();
161+
manager.resolveUserContext({});
162162
await flushPromises();
163163

164164
expect(client.onReady).not.toHaveBeenCalled();
@@ -239,7 +239,7 @@ describe('UserContextManager', () => {
239239
const config = createManagerConfig(client);
240240
const manager = new UserContextManager(config);
241241

242-
manager.resolveUserContext(); // no user
242+
manager.resolveUserContext({}); // empty object = VUID mode
243243
await flushPromises();
244244

245245
// Waiting on onReady for VUID
@@ -293,7 +293,7 @@ describe('UserContextManager', () => {
293293
const config = createManagerConfig(client);
294294
const manager = new UserContextManager(config);
295295

296-
manager.resolveUserContext(undefined, undefined, true); // no user, skipSegments
296+
manager.resolveUserContext({}, undefined, true); // empty object = VUID, skipSegments
297297
await flushPromises();
298298

299299
expect(client.onReady).toHaveBeenCalledTimes(1);
@@ -326,7 +326,7 @@ describe('UserContextManager', () => {
326326
const config = createManagerConfig(client);
327327
const manager = new UserContextManager(config);
328328

329-
manager.resolveUserContext(undefined, undefined, true); // no user, no VUID, skipSegments
329+
manager.resolveUserContext({}, undefined, true); // empty object, no VUID, skipSegments
330330
await flushPromises();
331331

332332
expect(client.onReady).not.toHaveBeenCalled();
@@ -531,12 +531,12 @@ describe('UserContextManager', () => {
531531
const config = createManagerConfig(client);
532532
const manager = new UserContextManager(config);
533533

534-
// First call — no userId, will wait for onReady
535-
manager.resolveUserContext();
534+
// First call — empty object (VUID), will wait for onReady
535+
manager.resolveUserContext({});
536536
await flushPromises();
537537

538-
// Second call — also no userId, should invalidate first
539-
manager.resolveUserContext();
538+
// Second call — also empty object, should invalidate first
539+
manager.resolveUserContext({});
540540
await flushPromises();
541541

542542
// Resolve onReady — both resume, but first is stale
@@ -564,8 +564,8 @@ describe('UserContextManager', () => {
564564
const config = createManagerConfig(client);
565565
const manager = new UserContextManager(config);
566566

567-
// First call — no userId, will wait for onReady
568-
manager.resolveUserContext();
567+
// First call — empty object (VUID), will wait for onReady
568+
manager.resolveUserContext({});
569569
await flushPromises();
570570
expect(client.onReady).toHaveBeenCalled();
571571

@@ -648,7 +648,7 @@ describe('UserContextManager', () => {
648648
const config = createManagerConfig(client);
649649
const manager = new UserContextManager(config);
650650

651-
manager.resolveUserContext(); // no userId, will await onReady
651+
manager.resolveUserContext({}); // empty object (VUID), will await onReady
652652
await flushPromises();
653653

654654
// Dispose before onReady resolves
@@ -701,7 +701,7 @@ describe('UserContextManager', () => {
701701
const config = createManagerConfig(client);
702702
const manager = new UserContextManager(config);
703703

704-
manager.resolveUserContext(); // no userId, will await onReady
704+
manager.resolveUserContext({}); // empty object (VUID), will await onReady
705705
await flushPromises();
706706

707707
manager.dispose();
@@ -727,7 +727,7 @@ describe('UserContextManager', () => {
727727
const config = createManagerConfig(client);
728728
const manager = new UserContextManager(config);
729729

730-
manager.resolveUserContext();
730+
manager.resolveUserContext({});
731731
await flushPromises();
732732

733733
onReadyDeferred.reject(new Error('SDK init failed'));
@@ -747,7 +747,7 @@ describe('UserContextManager', () => {
747747
const config = createManagerConfig(client);
748748
const manager = new UserContextManager(config);
749749

750-
manager.resolveUserContext();
750+
manager.resolveUserContext({});
751751
await flushPromises();
752752

753753
onReadyDeferred.reject('string error');
@@ -902,4 +902,165 @@ describe('UserContextManager', () => {
902902
manager.dispose();
903903
});
904904
});
905+
906+
// ============================================================
907+
// Null / undefined user (no-context guard)
908+
// ============================================================
909+
describe('null / undefined user (no-context guard)', () => {
910+
it('should not call createUserContext when user is undefined', async () => {
911+
const { client } = createMockClient({
912+
hasOdpManager: false,
913+
hasVuidManager: false,
914+
});
915+
const config = createManagerConfig(client);
916+
const manager = new UserContextManager(config);
917+
918+
manager.resolveUserContext(undefined);
919+
await flushPromises();
920+
921+
expect(client.createUserContext).not.toHaveBeenCalled();
922+
expect(config.onUserContextReady).toHaveBeenCalledWith(null);
923+
expect(config.onError).not.toHaveBeenCalled();
924+
925+
manager.dispose();
926+
});
927+
928+
it('should not call createUserContext when user is null', async () => {
929+
const { client } = createMockClient({
930+
hasOdpManager: false,
931+
hasVuidManager: false,
932+
});
933+
const config = createManagerConfig(client);
934+
const manager = new UserContextManager(config);
935+
936+
manager.resolveUserContext(null);
937+
await flushPromises();
938+
939+
expect(client.createUserContext).not.toHaveBeenCalled();
940+
expect(config.onUserContextReady).toHaveBeenCalledWith(null);
941+
expect(config.onError).not.toHaveBeenCalled();
942+
943+
manager.dispose();
944+
});
945+
946+
it('should call onUserContextReady(null) when user transitions from valid to null', async () => {
947+
const { client } = createMockClient({
948+
hasOdpManager: false,
949+
hasVuidManager: false,
950+
});
951+
const config = createManagerConfig(client);
952+
const manager = new UserContextManager(config);
953+
954+
manager.resolveUserContext({ id: 'user-1' });
955+
expect(config.onUserContextReady).toHaveBeenCalledTimes(1);
956+
expect(client.createUserContext).toHaveBeenCalledTimes(1);
957+
958+
manager.resolveUserContext(null);
959+
await flushPromises();
960+
961+
expect(config.onUserContextReady).toHaveBeenCalledTimes(2);
962+
expect(config.onUserContextReady).toHaveBeenLastCalledWith(null);
963+
expect(client.createUserContext).toHaveBeenCalledTimes(1);
964+
965+
manager.dispose();
966+
});
967+
968+
it('should cancel in-flight async work when user becomes null', async () => {
969+
const { client, onReadyDeferred } = createMockClient({
970+
hasOdpManager: false,
971+
hasVuidManager: true,
972+
});
973+
const config = createManagerConfig(client);
974+
const manager = new UserContextManager(config);
975+
976+
// Start with empty object (VUID flow) — async, waiting on onReady
977+
manager.resolveUserContext({});
978+
await flushPromises();
979+
expect(client.onReady).toHaveBeenCalled();
980+
981+
// User becomes null before onReady resolves
982+
manager.resolveUserContext(null);
983+
await flushPromises();
984+
985+
expect(config.onUserContextReady).toHaveBeenCalledTimes(1);
986+
expect(config.onUserContextReady).toHaveBeenCalledWith(null);
987+
988+
// onReady resolves — stale request should not fire callback
989+
onReadyDeferred.resolve(undefined);
990+
await flushPromises();
991+
992+
expect(config.onUserContextReady).toHaveBeenCalledTimes(1);
993+
expect(client.createUserContext).not.toHaveBeenCalled();
994+
995+
manager.dispose();
996+
});
997+
998+
it('should create context when user transitions from null to valid', async () => {
999+
const { client, mockUserContext } = createMockClient({
1000+
hasOdpManager: false,
1001+
hasVuidManager: false,
1002+
});
1003+
const config = createManagerConfig(client);
1004+
const manager = new UserContextManager(config);
1005+
1006+
manager.resolveUserContext(null);
1007+
await flushPromises();
1008+
1009+
expect(config.onUserContextReady).toHaveBeenCalledTimes(1);
1010+
expect(config.onUserContextReady).toHaveBeenCalledWith(null);
1011+
1012+
manager.resolveUserContext({ id: 'user-1' });
1013+
1014+
expect(client.createUserContext).toHaveBeenCalledWith('user-1', undefined);
1015+
expect(config.onUserContextReady).toHaveBeenCalledTimes(2);
1016+
expect(config.onUserContextReady).toHaveBeenLastCalledWith(mockUserContext);
1017+
1018+
manager.dispose();
1019+
});
1020+
1021+
it('should short-circuit when user stays null', async () => {
1022+
const { client } = createMockClient({
1023+
hasOdpManager: false,
1024+
hasVuidManager: false,
1025+
});
1026+
const config = createManagerConfig(client);
1027+
const manager = new UserContextManager(config);
1028+
1029+
manager.resolveUserContext(null);
1030+
await flushPromises();
1031+
1032+
expect(config.onUserContextReady).toHaveBeenCalledTimes(1);
1033+
1034+
// Call again with null — should short-circuit
1035+
manager.resolveUserContext(null);
1036+
await flushPromises();
1037+
1038+
expect(config.onUserContextReady).toHaveBeenCalledTimes(1);
1039+
expect(client.createUserContext).not.toHaveBeenCalled();
1040+
1041+
manager.dispose();
1042+
});
1043+
1044+
it('should trigger VUID flow when user is empty object', async () => {
1045+
const { client, mockUserContext, onReadyDeferred } = createMockClient({
1046+
hasOdpManager: false,
1047+
hasVuidManager: true,
1048+
});
1049+
const config = createManagerConfig(client);
1050+
const manager = new UserContextManager(config);
1051+
1052+
manager.resolveUserContext({});
1053+
await flushPromises();
1054+
1055+
expect(client.onReady).toHaveBeenCalled();
1056+
1057+
onReadyDeferred.resolve(undefined);
1058+
await flushPromises();
1059+
1060+
expect(client.createUserContext).toHaveBeenCalledWith(undefined, undefined);
1061+
expect(config.onUserContextReady).toHaveBeenCalledWith(mockUserContext);
1062+
1063+
manager.dispose();
1064+
});
1065+
});
9051066
});

0 commit comments

Comments
 (0)