@ -1,13 +1,11 @@
import { mock } from "jest-mock-extended" ;
import { BehaviorSubject, firstValueFrom } from "rxjs" ;
import { firstValueFrom } from "rxjs" ;
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions" ;
import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response" ;
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service" ;
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service" ;
import { newGuid } from "@bitwarden/guid" ;
import { FeatureFlag } from "../../../enums/feature-flag.enum" ;
import { OrganizationId } from "../../../types/guid" ;
import { DefaultOrganizationMetadataService } from "./organization-metadata.service" ;
@ -15,9 +13,7 @@ import { DefaultOrganizationMetadataService } from "./organization-metadata.serv
describe ( "DefaultOrganizationMetadataService" , ( ) = > {
let service : DefaultOrganizationMetadataService ;
let billingApiService : jest.Mocked < BillingApiServiceAbstraction > ;
let configService : jest.Mocked < ConfigService > ;
let platformUtilsService : jest.Mocked < PlatformUtilsService > ;
let featureFlagSubject : BehaviorSubject < boolean > ;
const mockOrganizationId = newGuid ( ) as OrganizationId ;
const mockOrganizationId2 = newGuid ( ) as OrganizationId ;
@ -34,182 +30,114 @@ describe("DefaultOrganizationMetadataService", () => {
beforeEach ( ( ) = > {
billingApiService = mock < BillingApiServiceAbstraction > ( ) ;
configService = mock < ConfigService > ( ) ;
platformUtilsService = mock < PlatformUtilsService > ( ) ;
featureFlagSubject = new BehaviorSubject < boolean > ( false ) ;
configService . getFeatureFlag $ . mockReturnValue ( featureFlagSubject . asObservable ( ) ) ;
platformUtilsService . isSelfHost . mockReturnValue ( false ) ;
service = new DefaultOrganizationMetadataService (
billingApiService ,
configService ,
platformUtilsService ,
) ;
service = new DefaultOrganizationMetadataService ( billingApiService , platformUtilsService ) ;
} ) ;
afterEach ( ( ) = > {
jest . resetAllMocks ( ) ;
featureFlagSubject . complete ( ) ;
} ) ;
describe ( "getOrganizationMetadata$" , ( ) = > {
describe ( "feature flag OFF" , ( ) = > {
beforeEach ( ( ) = > {
featureFlagSubject . next ( false ) ;
} ) ;
it ( "calls getOrganizationBillingMetadata when feature flag is off" , async ( ) = > {
const mockResponse = createMockMetadataResponse ( false , 10 ) ;
billingApiService . getOrganizationBillingMetadata . mockResolvedValue ( mockResponse ) ;
const result = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
expect ( configService . getFeatureFlag $ ) . toHaveBeenCalledWith (
FeatureFlag . PM25379_UseNewOrganizationMetadataStructure ,
) ;
expect ( billingApiService . getOrganizationBillingMetadata ) . toHaveBeenCalledWith (
mockOrganizationId ,
) ;
expect ( billingApiService . getOrganizationBillingMetadataVNext ) . not . toHaveBeenCalled ( ) ;
expect ( result ) . toEqual ( mockResponse ) ;
} ) ;
it ( "does not cache metadata when feature flag is off" , async ( ) = > {
const mockResponse1 = createMockMetadataResponse ( false , 10 ) ;
const mockResponse2 = createMockMetadataResponse ( false , 15 ) ;
billingApiService . getOrganizationBillingMetadata
. mockResolvedValueOnce ( mockResponse1 )
. mockResolvedValueOnce ( mockResponse2 ) ;
it ( "calls getOrganizationBillingMetadata for cloud-hosted" , async ( ) = > {
const mockResponse = createMockMetadataResponse ( false , 10 ) ;
billingApiService . getOrganizationBillingMetadata . mockResolvedValue ( mockResponse ) ;
const result1 = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
const result2 = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
const result = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
expect ( billingApiService . getOrganizationBillingMetadata ) . toHaveBeenCalled Times( 2 ) ;
expect( result1 ) . toEqual ( mockResponse1 ) ;
expect ( result2 ) . toEqual ( mockResponse2 ) ;
} ) ;
expect ( billingApiService . getOrganizationBillingMetadata ) . toHaveBeenCalledWith (
mockOrganizationId ,
) ;
expect ( result ) . toEqual ( mockResponse ) ;
} ) ;
describe( "feature flag ON" , ( ) = > {
beforeEach( ( ) = > {
featureFlagSubject . next ( true ) ;
} ) ;
it ( "calls getOrganizationBillingMetadataSelfHost when isSelfHost is true" , async ( ) = > {
platformUtilsService . isSelfHost . mockReturnValue ( true ) ;
const mockResponse = createMockMetadataResponse ( true , 25 ) ;
billingApiService . getOrganizationBillingMetadataSelfHost . mockResolvedValue ( mockResponse ) ;
it ( "calls getOrganizationBillingMetadataVNext when feature flag is on" , async ( ) = > {
const mockResponse = createMockMetadataResponse ( true , 15 ) ;
billingApiService . getOrganizationBillingMetadataVNext . mockResolvedValue ( mockResponse ) ;
const result = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
const result = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
expect ( platformUtilsService . isSelfHost ) . toHaveBeenCalled ( ) ;
expect ( billingApiService . getOrganizationBillingMetadataSelfHost ) . toHaveBeenCalledWith (
mockOrganizationId ,
) ;
expect ( billingApiService . getOrganizationBillingMetadata ) . not . toHaveBeenCalled ( ) ;
expect ( result ) . toEqual ( mockResponse ) ;
} ) ;
expect ( configService . getFeatureFlag $ ) . toHaveBeenCalledWith (
FeatureFlag . PM25379_UseNewOrganizationMetadataStructure ,
) ;
expect ( billingApiService . getOrganizationBillingMetadataVNext ) . toHaveBeenCalledWith (
mockOrganizationId ,
) ;
expect ( billingApiService . getOrganizationBillingMetadata ) . not . toHaveBeenCalled ( ) ;
expect ( result ) . toEqual ( mockResponse ) ;
} ) ;
it ( "caches metadata by organization ID" , async ( ) = > {
const mockResponse = createMockMetadataResponse ( true , 10 ) ;
billingApiService . getOrganizationBillingMetadata . mockResolvedValue ( mockResponse ) ;
it ( "caches metadata by organization ID when feature flag is on" , async ( ) = > {
const mockResponse = createMockMetadataResponse ( true , 10 ) ;
billingApiService . getOrganizationBillingMetadataVNext . mockResolvedValue ( mockResponse ) ;
const result1 = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
const result2 = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
const result1 = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
const result2 = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
expect ( billingApiService . getOrganizationBillingMetadata ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( result1 ) . toEqual ( mockResponse ) ;
expect ( result2 ) . toEqual ( mockResponse ) ;
} ) ;
expect ( billingApiService . getOrganizationBillingMetadataVNext ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( result1 ) . toEqual ( mockResponse ) ;
expect ( result2 ) . toEqual ( mockResponse ) ;
} ) ;
it ( "maintains separate cache entries for different organization IDs" , async ( ) = > {
const mockResponse1 = createMockMetadataResponse ( true , 10 ) ;
const mockResponse2 = createMockMetadataResponse ( false , 20 ) ;
billingApiService . getOrganizationBillingMetadata
. mockResolvedValueOnce ( mockResponse1 )
. mockResolvedValueOnce ( mockResponse2 ) ;
it ( "maintains separate cache entries for different organization IDs" , async ( ) = > {
const mockResponse1 = createMockMetadataResponse ( true , 10 ) ;
const mockResponse2 = createMockMetadataResponse ( false , 20 ) ;
billingApiService . getOrganizationBillingMetadataVNext
. mockResolvedValueOnce ( mockResponse1 )
. mockResolvedValueOnce ( mockResponse2 ) ;
const result1 = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
const result2 = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId2 ) ) ;
const result3 = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
const result4 = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId2 ) ) ;
expect ( billingApiService . getOrganizationBillingMetadataVNext ) . toHaveBeenCalledTimes ( 2 ) ;
expect ( billingApiService . getOrganizationBillingMetadataVNext ) . toHaveBeenNthCalledWith (
1 ,
mockOrganizationId ,
) ;
expect ( billingApiService . getOrganizationBillingMetadataVNext ) . toHaveBeenNthCalledWith (
2 ,
mockOrganizationId2 ,
) ;
expect ( result1 ) . toEqual ( mockResponse1 ) ;
expect ( result2 ) . toEqual ( mockResponse2 ) ;
expect ( result3 ) . toEqual ( mockResponse1 ) ;
expect ( result4 ) . toEqual ( mockResponse2 ) ;
} ) ;
const result1 = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
const result2 = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId2 ) ) ;
const result3 = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
const result4 = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId2 ) ) ;
it ( "calls getOrganizationBillingMetadataVNextSelfHost when feature flag is on and isSelfHost is true" , async ( ) = > {
platformUtilsService . isSelfHost . mockReturnValue ( true ) ;
const mockResponse = createMockMetadataResponse ( true , 25 ) ;
billingApiService . getOrganizationBillingMetadataVNextSelfHost . mockResolvedValue (
mockResponse ,
) ;
const result = await firstValueFrom ( service . getOrganizationMetadata $ ( mockOrganizationId ) ) ;
expect ( platformUtilsService . isSelfHost ) . toHaveBeenCalled ( ) ;
expect ( billingApiService . getOrganizationBillingMetadataVNextSelfHost ) . toHaveBeenCalledWith (
mockOrganizationId ,
) ;
expect ( billingApiService . getOrganizationBillingMetadataVNext ) . not . toHaveBeenCalled ( ) ;
expect ( billingApiService . getOrganizationBillingMetadata ) . not . toHaveBeenCalled ( ) ;
expect ( result ) . toEqual ( mockResponse ) ;
} ) ;
expect ( billingApiService . getOrganizationBillingMetadata ) . toHaveBeenCalledTimes ( 2 ) ;
expect ( billingApiService . getOrganizationBillingMetadata ) . toHaveBeenNthCalledWith (
1 ,
mockOrganizationId ,
) ;
expect ( billingApiService . getOrganizationBillingMetadata ) . toHaveBeenNthCalledWith (
2 ,
mockOrganizationId2 ,
) ;
expect ( result1 ) . toEqual ( mockResponse1 ) ;
expect ( result2 ) . toEqual ( mockResponse2 ) ;
expect ( result3 ) . toEqual ( mockResponse1 ) ;
expect ( result4 ) . toEqual ( mockResponse2 ) ;
} ) ;
describe ( "shareReplay behavior" , ( ) = > {
beforeEach ( ( ) = > {
featureFlagSubject . next ( true ) ;
} ) ;
it ( "does not call API multiple times when the same cached observable is subscribed to multiple times" , async ( ) = > {
const mockResponse = createMockMetadataResponse ( true , 10 ) ;
billingApiService . getOrganizationBillingMetadataVNext . mockResolvedValue ( mockResponse ) ;
it ( "does not call API multiple times when the same cached observable is subscribed to multiple times" , async ( ) = > {
const mockResponse = createMockMetadataResponse ( true , 10 ) ;
billingApiService . getOrganizationBillingMetadata . mockResolvedValue ( mockResponse ) ;
const metadata $ = service . getOrganizationMetadata $ ( mockOrganizationId ) ;
const metadata $ = service . getOrganizationMetadata $ ( mockOrganizationId ) ;
const subscription1Promise = firstValueFrom ( metadata $ ) ;
const subscription2Promise = firstValueFrom ( metadata $ ) ;
const subscription3Promise = firstValueFrom ( metadata $ ) ;
const subscription1Promise = firstValueFrom ( metadata $ ) ;
const subscription2Promise = firstValueFrom ( metadata $ ) ;
const subscription3Promise = firstValueFrom ( metadata $ ) ;
const [ result1 , result2 , result3 ] = await Promise . all ( [
subscription1Promise ,
subscription2Promise ,
subscription3Promise ,
] ) ;
const [ result1 , result2 , result3 ] = await Promise . all ( [
subscription1Promise ,
subscription2Promise ,
subscription3Promise ,
] ) ;
expect ( billingApiService . getOrganizationBillingMetadataVNext ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( result1 ) . toEqual ( mockResponse ) ;
expect ( result2 ) . toEqual ( mockResponse ) ;
expect ( result3 ) . toEqual ( mockResponse ) ;
} ) ;
expect ( billingApiService . getOrganizationBillingMetadata ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( result1 ) . toEqual ( mockResponse ) ;
expect ( result2 ) . toEqual ( mockResponse ) ;
expect ( result3 ) . toEqual ( mockResponse ) ;
} ) ;
} ) ;
describe ( "refreshMetadataCache" , ( ) = > {
beforeEach ( ( ) = > {
featureFlagSubject . next ( true ) ;
} ) ;
it ( "refreshes cached metadata when called with feature flag on" , ( done ) = > {
it ( "refreshes cached metadata when called" , ( done ) = > {
const mockResponse1 = createMockMetadataResponse ( true , 10 ) ;
const mockResponse2 = createMockMetadataResponse ( true , 20 ) ;
let invocationCount = 0 ;
billingApiService . getOrganizationBillingMetadata VNext
billingApiService . getOrganizationBillingMetadata
. mockResolvedValueOnce ( mockResponse1 )
. mockResolvedValueOnce ( mockResponse2 ) ;
@ -221,7 +149,7 @@ describe("DefaultOrganizationMetadataService", () => {
expect ( result ) . toEqual ( mockResponse1 ) ;
} else if ( invocationCount === 2 ) {
expect ( result ) . toEqual ( mockResponse2 ) ;
expect ( billingApiService . getOrganizationBillingMetadata VNext ) . toHaveBeenCalledTimes ( 2 ) ;
expect ( billingApiService . getOrganizationBillingMetadata ) . toHaveBeenCalledTimes ( 2 ) ;
subscription . unsubscribe ( ) ;
done ( ) ;
}
@ -234,45 +162,13 @@ describe("DefaultOrganizationMetadataService", () => {
} , 10 ) ;
} ) ;
it ( "does trigger refresh when feature flag is disabled" , async ( ) = > {
featureFlagSubject . next ( false ) ;
const mockResponse1 = createMockMetadataResponse ( false , 10 ) ;
const mockResponse2 = createMockMetadataResponse ( false , 20 ) ;
let invocationCount = 0 ;
billingApiService . getOrganizationBillingMetadata
. mockResolvedValueOnce ( mockResponse1 )
. mockResolvedValueOnce ( mockResponse2 ) ;
const subscription = service . getOrganizationMetadata $ ( mockOrganizationId ) . subscribe ( {
next : ( ) = > {
invocationCount ++ ;
} ,
} ) ;
// wait for initial invocation
await new Promise ( ( resolve ) = > setTimeout ( resolve , 10 ) ) ;
expect ( invocationCount ) . toBe ( 1 ) ;
service . refreshMetadataCache ( ) ;
await new Promise ( ( resolve ) = > setTimeout ( resolve , 10 ) ) ;
expect ( invocationCount ) . toBe ( 2 ) ;
expect ( billingApiService . getOrganizationBillingMetadata ) . toHaveBeenCalledTimes ( 2 ) ;
subscription . unsubscribe ( ) ;
} ) ;
it ( "bypasses cache when refreshing metadata" , ( done ) = > {
const mockResponse1 = createMockMetadataResponse ( true , 10 ) ;
const mockResponse2 = createMockMetadataResponse ( true , 20 ) ;
const mockResponse3 = createMockMetadataResponse ( true , 30 ) ;
let invocationCount = 0 ;
billingApiService . getOrganizationBillingMetadata VNext
billingApiService . getOrganizationBillingMetadata
. mockResolvedValueOnce ( mockResponse1 )
. mockResolvedValueOnce ( mockResponse2 )
. mockResolvedValueOnce ( mockResponse3 ) ;
@ -289,7 +185,7 @@ describe("DefaultOrganizationMetadataService", () => {
service . refreshMetadataCache ( ) ;
} else if ( invocationCount === 3 ) {
expect ( result ) . toEqual ( mockResponse3 ) ;
expect ( billingApiService . getOrganizationBillingMetadata VNext ) . toHaveBeenCalledTimes ( 3 ) ;
expect ( billingApiService . getOrganizationBillingMetadata ) . toHaveBeenCalledTimes ( 3 ) ;
subscription . unsubscribe ( ) ;
done ( ) ;
}