-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
251 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store'; | ||
import { Flag, FormatEnum, ObfuscatedFlag, VariationType } from '../interfaces'; | ||
import * as overrideValidatorModule from '../override-validator'; | ||
|
||
import EppoClient from './eppo-client'; | ||
|
||
describe('EppoClient', () => { | ||
const storage = new MemoryOnlyConfigurationStore<Flag | ObfuscatedFlag>(); | ||
|
||
function setUnobfuscatedFlagEntries( | ||
entries: Record<string, Flag | ObfuscatedFlag>, | ||
): Promise<boolean> { | ||
storage.setFormat(FormatEnum.SERVER); | ||
return storage.setEntries(entries); | ||
} | ||
|
||
const flagKey = 'mock-flag'; | ||
|
||
const variationA = { | ||
key: 'a', | ||
value: 'variation-a', | ||
}; | ||
|
||
const variationB = { | ||
key: 'b', | ||
value: 'variation-b', | ||
}; | ||
|
||
const mockFlag: Flag = { | ||
key: flagKey, | ||
enabled: true, | ||
variationType: VariationType.STRING, | ||
variations: { a: variationA, b: variationB }, | ||
allocations: [ | ||
{ | ||
key: 'allocation-a', | ||
rules: [], | ||
splits: [ | ||
{ | ||
shards: [], | ||
variationKey: 'a', | ||
}, | ||
], | ||
doLog: true, | ||
}, | ||
], | ||
totalShards: 10000, | ||
}; | ||
|
||
let client: EppoClient; | ||
let subjectKey: string; | ||
|
||
beforeEach(async () => { | ||
await setUnobfuscatedFlagEntries({ [flagKey]: mockFlag }); | ||
subjectKey = 'subject-10'; | ||
client = new EppoClient({ flagConfigurationStore: storage }); | ||
}); | ||
|
||
describe('parseOverrides', () => { | ||
it('should parse a valid payload', async () => { | ||
jest.spyOn(overrideValidatorModule, 'sendValidationRequest').mockResolvedValue(undefined); | ||
const result = await client.parseOverrides( | ||
JSON.stringify({ | ||
browserExtensionKey: 'my-key', | ||
overrides: { [flagKey]: variationB }, | ||
}), | ||
); | ||
expect(result).toEqual({ [flagKey]: variationB }); | ||
}); | ||
|
||
it('should throw an error if the key is missing', async () => { | ||
jest.spyOn(overrideValidatorModule, 'sendValidationRequest').mockResolvedValue(undefined); | ||
expect(() => | ||
client.parseOverrides( | ||
JSON.stringify({ | ||
overrides: { [flagKey]: variationB }, | ||
}), | ||
), | ||
).rejects.toThrow(); | ||
}); | ||
|
||
it('should throw an error if the key is not a string', async () => { | ||
jest.spyOn(overrideValidatorModule, 'sendValidationRequest').mockResolvedValue(undefined); | ||
expect(() => | ||
client.parseOverrides( | ||
JSON.stringify({ | ||
browserExtensionKey: 123, | ||
overrides: { [flagKey]: variationB }, | ||
}), | ||
), | ||
).rejects.toThrow(); | ||
}); | ||
|
||
it('should throw an error if the overrides are not parseable', async () => { | ||
jest.spyOn(overrideValidatorModule, 'sendValidationRequest').mockResolvedValue(undefined); | ||
expect(() => | ||
client.parseOverrides(`{ | ||
browserExtensionKey: 'my-key', | ||
overrides: { [flagKey]: , | ||
}`), | ||
).rejects.toThrow(); | ||
}); | ||
|
||
it('should throw an error if overrides is not an object', async () => { | ||
jest.spyOn(overrideValidatorModule, 'sendValidationRequest').mockResolvedValue(undefined); | ||
expect(() => | ||
client.parseOverrides( | ||
JSON.stringify({ | ||
browserExtensionKey: 'my-key', | ||
overrides: false, | ||
}), | ||
), | ||
).rejects.toThrow(); | ||
}); | ||
|
||
it('should throw an error if an invalid key is supplied', async () => { | ||
jest.spyOn(overrideValidatorModule, 'sendValidationRequest').mockImplementation(async () => { | ||
throw new Error(`Unable to authorize key`); | ||
}); | ||
expect(() => | ||
client.parseOverrides( | ||
JSON.stringify({ | ||
browserExtensionKey: 'my-key', | ||
overrides: { [flagKey]: variationB }, | ||
}), | ||
), | ||
).rejects.toThrow(); | ||
}); | ||
}); | ||
|
||
describe('withOverrides', () => { | ||
it('should create a new instance of EppoClient with specified overrides without affecting the original instance', () => { | ||
const clientWithOverrides = client.withOverrides({ [flagKey]: variationB }); | ||
|
||
expect(client.getStringAssignment(flagKey, subjectKey, {}, 'default')).toBe('variation-a'); | ||
expect(clientWithOverrides.getStringAssignment(flagKey, subjectKey, {}, 'default')).toBe( | ||
'variation-b', | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { TLRUCache } from './cache/tlru-cache'; | ||
import { Variation } from './interfaces'; | ||
import { FlagKey } from './types'; | ||
|
||
const FIVE_MINUTES_IN_MS = 5 * 3600 * 1000; | ||
const KEY_VALIDATION_URL = 'https://eppo.cloud/api/flag-overrides/v1/validate-key'; | ||
|
||
export interface OverridePayload { | ||
browserExtensionKey: string; | ||
overrides: Record<FlagKey, Variation>; | ||
} | ||
|
||
export const sendValidationRequest = async (browserExtensionKey: string) => { | ||
const response = await fetch(KEY_VALIDATION_URL, { | ||
method: 'POST', | ||
body: JSON.stringify({ | ||
key: browserExtensionKey, | ||
}), | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
}); | ||
if (response.status !== 200) { | ||
throw new Error(`Unable to authorize key: ${response.statusText}`); | ||
} | ||
}; | ||
|
||
export class OverrideValidator { | ||
private validApiKeyCache = new TLRUCache(100, FIVE_MINUTES_IN_MS); | ||
|
||
parseOverridePayload(overridePayload: string): OverridePayload { | ||
const errorMsg = (msg: string) => `Unable to parse overridePayload: ${msg}`; | ||
try { | ||
const parsed = JSON.parse(overridePayload); | ||
this.validateParsedOverridePayload(parsed); | ||
return parsed as OverridePayload; | ||
} catch (err: unknown) { | ||
const message: string = (err as any)?.message ?? 'unknown error'; | ||
throw new Error(errorMsg(message)); | ||
} | ||
} | ||
|
||
private validateParsedOverridePayload(parsed: any) { | ||
if (typeof parsed !== 'object') { | ||
throw new Error(`Expected object, but received ${typeof parsed}`); | ||
} | ||
const keys = Object.keys(parsed); | ||
if (!keys.includes('browserExtensionKey')) { | ||
throw new Error(`Missing required field: 'browserExtensionKey'`); | ||
} | ||
if (!keys.includes('overrides')) { | ||
throw new Error(`Missing required field: 'overrides'`); | ||
} | ||
if (typeof parsed['browserExtensionKey'] !== 'string') { | ||
throw new Error( | ||
`Invalid type for 'browserExtensionKey'. Expected string, but received ${typeof parsed['browserExtensionKey']}`, | ||
); | ||
} | ||
if (typeof parsed['overrides'] !== 'object') { | ||
throw new Error( | ||
`Invalid type for 'overrides'. Expected object, but received ${typeof parsed['overrides']}.`, | ||
); | ||
} | ||
} | ||
|
||
async validateOverrideApiKey(overrideApiKey: string) { | ||
if (this.validApiKeyCache.get(overrideApiKey) === 'true') { | ||
return true; | ||
} | ||
await sendValidationRequest(overrideApiKey); | ||
this.validApiKeyCache.set(overrideApiKey, 'true'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters