Repository: open-feature/js-sdk (ts)

Issue:
[FEATURE] In-memory provider for js (node) and web SDKs
### Requirements

As outlined [in the specification](https://openfeature.dev/specification/appendix-a#in-memory-provider), the js-sdk should include an in-memory provider implementation. This implementation should take a simple object that defines flags as well as simple rules in the form of lambdas (see equivalent [Java implementation](https://github.com/open-feature/java-sdk/blob/main/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java), and existing [JS implementation](https://github.com/open-feature/js-sdk-contrib/tree/main/libs/providers/in-memory) in contrib) and use those to evaluate flags. This is very useful to conusmers of the SDK for testing purposes. Please carefully implement all the behaviors defined in the spec for the in-memory provider.

This is a great issue to take on if you're looking to get started in OpenFeature!

@moredip This would be pretty similar to what you implemented in the JS contrib. I think the main thing missing would be rules-based evaluation (some flag "values" would instead define lambdas to run which are passed the context). If you'd like to work on this issue, just assign it to yourself. Otherwise, I will add the `good first issue`, etc tags to it which I suspect will result in it quickly being picked up.
```[tasklist]
### Tasks
- [ ] web-sdk implementation (`packages/client`)
- [x] js-sdk (node) implementation (`packages/server`)
- [ ] remove existing partial implementation from js-sdk-contrib
```


Pull request:
feat: client in memory provider
## This PR

- Adds in memory provider implementation for client (web) sdk
- Move files from server sdk to shared

### Related Issues

Closes #565 

### Notes

It's a first try. Any feedback and enhancement proposals are welcome :)

### How to test

Automated/unit testes were implemented, but e2e tests wouldn't hurt.




Patch:
diff --git a/packages/client/src/provider/in-memory-provider/flag-configuration.ts b/packages/client/src/provider/in-memory-provider/flag-configuration.ts
new file mode 100644
index 00000000..2bbfc917
--- /dev/null
+++ b/packages/client/src/provider/in-memory-provider/flag-configuration.ts
@@ -0,0 +1,36 @@
+/**
+ * Don't export types from this file publicly.
+ * It might cause confusion since these types are not a part of the general API,
+ * but just for the in-memory provider.
+ */
+import { EvaluationContext, JsonValue } from '@openfeature/core';
+
+type Variants<T> = Record<string, T>;
+
+/**
+ * A Feature Flag definition, containing it's specification
+ */
+export type Flag = {
+  /**
+   * An object containing all possible flags mappings (variant -> flag value)
+   */
+  variants: Variants<boolean> | Variants<string> | Variants<number> | Variants<JsonValue>;
+  /**
+   * The variant it will resolve to in STATIC evaluation
+   */
+  defaultVariant: string;
+  /**
+   * Determines if flag evaluation is enabled or not for this flag.
+   * If false, falls back to the default value provided to the client
+   */
+  disabled: boolean;
+  /**
+   * Function used in order to evaluate a flag to a specific value given the provided context.
+   * It should return a variant key.
+   * If it does not return a valid variant it falls back to the default value provided to the client
+   * @param EvaluationContext
+   */
+  contextEvaluator?: (ctx: EvaluationContext) => string;
+};
+
+export type FlagConfiguration = Record<string, Flag>;
diff --git a/packages/client/src/provider/in-memory-provider/in-memory-provider.ts b/packages/client/src/provider/in-memory-provider/in-memory-provider.ts
new file mode 100644
index 00000000..46a8165f
--- /dev/null
+++ b/packages/client/src/provider/in-memory-provider/in-memory-provider.ts
@@ -0,0 +1,179 @@
+import {
+  EvaluationContext,
+  FlagNotFoundError,
+  FlagValueType,
+  GeneralError,
+  JsonValue,
+  Logger,
+  OpenFeatureError,
+  ProviderEvents,
+  ResolutionDetails,
+  StandardResolutionReasons,
+  TypeMismatchError,
+  ProviderStatus,
+} from '@openfeature/core';
+import { Provider } from '../provider';
+import { OpenFeatureEventEmitter } from '../../events';
+import { FlagConfiguration, Flag } from './flag-configuration';
+import { VariantNotFoundError } from './variant-not-found-error';
+
+/**
+ * A simple OpenFeature provider intended for demos and as a test stub.
+ */
+export class InMemoryProvider implements Provider {
+  public readonly events = new OpenFeatureEventEmitter();
+  public readonly runsOn = 'client';
+  status: ProviderStatus = ProviderStatus.NOT_READY;
+  readonly metadata = {
+    name: 'in-memory',
+  } as const;
+  private _flagConfiguration: FlagConfiguration;
+  private _context: EvaluationContext | undefined;
+
+  constructor(flagConfiguration: FlagConfiguration = {}) {
+    this._flagConfiguration = { ...flagConfiguration };
+  }
+
+  async initialize(context?: EvaluationContext | undefined): Promise<void> {
+    try {
+
+      for (const key in this._flagConfiguration) {
+        this.resolveFlagWithReason(key, context);
+      }
+
+      this._context = context;
+      // set the provider's state, but don't emit events manually;
+      // the SDK does this based on the resolution/rejection of the init promise
+      this.status = ProviderStatus.READY;
+    } catch (error) {
+      this.status = ProviderStatus.ERROR;
+      throw error;
+    }
+  }
+
+  /**
+   * Overwrites the configured flags.
+   * @param { FlagConfiguration } flagConfiguration new flag configuration
+   */
+  async putConfiguration(flagConfiguration: FlagConfiguration) {
+    const flagsChanged = Object.entries(flagConfiguration)
+      .filter(([key, value]) => this._flagConfiguration[key] !== value)
+      .map(([key]) => key);
+
+    this.status = ProviderStatus.STALE;
+    this.events.emit(ProviderEvents.Stale);
+
+    this._flagConfiguration = { ...flagConfiguration };
+    this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged });
+
+    try {
+      await this.initialize(this._context);
+      // we need to emit our own events in this case, since it's not part of the init flow.
+      this.events.emit(ProviderEvents.Ready);
+    } catch (err) {
+      this.events.emit(ProviderEvents.Error);
+      throw err;
+    }
+  }
+
+  resolveBooleanEvaluation(
+    flagKey: string,
+    defaultValue: boolean,
+    context?: EvaluationContext,
+    logger?: Logger,
+  ): ResolutionDetails<boolean> {
+    return this.resolveAndCheckFlag<boolean>(flagKey, defaultValue, context || this._context, logger);
+  }
+
+  resolveNumberEvaluation(
+    flagKey: string,
+    defaultValue: number,
+    context?: EvaluationContext,
+    logger?: Logger,
+  ): ResolutionDetails<number> {
+    return this.resolveAndCheckFlag<number>(flagKey, defaultValue, context || this._context, logger);
+  }
+
+  resolveStringEvaluation(
+    flagKey: string,
+    defaultValue: string,
+    context?: EvaluationContext,
+    logger?: Logger,
+  ): ResolutionDetails<string> {
+    return this.resolveAndCheckFlag<string>(flagKey, defaultValue, context || this._context, logger);
+  }
+
+  resolveObjectEvaluation<T extends JsonValue>(
+    flagKey: string,
+    defaultValue: T,
+    context?: EvaluationContext,
+    logger?: Logger,
+  ): ResolutionDetails<T> {
+    return this.resolveAndCheckFlag<T>(flagKey, defaultValue, context || this._context, logger);
+  }
+
+  private resolveAndCheckFlag<T extends JsonValue | FlagValueType>(flagKey: string,
+    defaultValue: T, context?: EvaluationContext, logger?: Logger): ResolutionDetails<T> {
+    if (!(flagKey in this._flagConfiguration)) {
+      const message = `no flag found with key ${flagKey}`;
+      logger?.debug(message);
+      throw new FlagNotFoundError(message);
+    }
+
+    if (this._flagConfiguration[flagKey].disabled) {
+      return { value: defaultValue, reason: StandardResolutionReasons.DISABLED };
+    }
+
+    const resolvedFlag = this.resolveFlagWithReason(flagKey, context) as ResolutionDetails<T>;
+
+    if (resolvedFlag.value === undefined) {
+      const message = `no value associated with variant provided for ${flagKey} found`;
+      logger?.error(message);
+      throw new VariantNotFoundError(message);
+    }
+
+    if (typeof resolvedFlag.value != typeof defaultValue) {
+      throw new TypeMismatchError();
+    }
+
+    return resolvedFlag;
+  }
+
+  private resolveFlagWithReason<T extends JsonValue | FlagValueType>(
+    flagKey: string,
+    ctx?: EvaluationContext,
+  ): ResolutionDetails<T> {
+    try {
+      const resolutionResult = this.lookupFlagValue<T>(flagKey, ctx);
+
+      return resolutionResult;
+    } catch (error: unknown) {
+      if (!(error instanceof OpenFeatureError)) {
+        throw new GeneralError((error as Error)?.message || 'unknown error');
+      }
+      throw error;
+    }
+  }
+
+  private lookupFlagValue<T extends JsonValue | FlagValueType>(
+    flagKey: string,
+    ctx?: EvaluationContext,
+  ): ResolutionDetails<T> {
+    const flagSpec: Flag = this._flagConfiguration[flagKey];
+
+    const isContextEval = ctx && flagSpec?.contextEvaluator;
+    const variant = isContextEval ? flagSpec.contextEvaluator?.(ctx) : flagSpec.defaultVariant;
+
+    const value = variant && flagSpec?.variants[variant];
+
+    const evalReason = isContextEval ? StandardResolutionReasons.TARGETING_MATCH : StandardResolutionReasons.STATIC;
+
+    const reason = this.status === ProviderStatus.STALE ? StandardResolutionReasons.CACHED : evalReason;
+
+    return {
+      value: value as T,
+      ...(variant && { variant }),
+      reason,
+    };
+  }
+}
diff --git a/packages/client/src/provider/in-memory-provider/index.ts b/packages/client/src/provider/in-memory-provider/index.ts
new file mode 100644
index 00000000..1bf0d537
--- /dev/null
+++ b/packages/client/src/provider/in-memory-provider/index.ts
@@ -0,0 +1,1 @@
+export * from './in-memory-provider';
diff --git a/packages/client/src/provider/in-memory-provider/variant-not-found-error.ts b/packages/client/src/provider/in-memory-provider/variant-not-found-error.ts
new file mode 100644
index 00000000..8b643982
--- /dev/null
+++ b/packages/client/src/provider/in-memory-provider/variant-not-found-error.ts
@@ -0,0 +1,15 @@
+import { ErrorCode, OpenFeatureError } from '@openfeature/core';
+
+/**
+ * A custom error for the in-memory provider.
+ * Indicates the resolved or default variant doesn't exist.
+ */
+export class VariantNotFoundError extends OpenFeatureError {
+  code: ErrorCode;
+  constructor(message?: string) {
+    super(message);
+    Object.setPrototypeOf(this, VariantNotFoundError.prototype);
+    this.name = 'VariantNotFoundError';
+    this.code = ErrorCode.GENERAL;
+  }
+}
diff --git a/packages/client/src/provider/index.ts b/packages/client/src/provider/index.ts
index 79eff210..33e12fea 100644
--- a/packages/client/src/provider/index.ts
+++ b/packages/client/src/provider/index.ts
@@ -1,2 +1,3 @@
 export * from './provider';
 export * from './no-op-provider';
+export * from './in-memory-provider';


Test patch:
diff --git a/packages/client/test/in-memory-provider.spec.ts b/packages/client/test/in-memory-provider.spec.ts
new file mode 100644
index 00000000..23854cbc
--- /dev/null
+++ b/packages/client/test/in-memory-provider.spec.ts
@@ -0,0 +1,602 @@
+import { FlagNotFoundError, GeneralError, ProviderEvents, ProviderStatus, StandardResolutionReasons, TypeMismatchError } from '@openfeature/core';
+import { InMemoryProvider } from '../src';
+import { FlagConfiguration } from '../src/provider/in-memory-provider/flag-configuration';
+import { VariantNotFoundError } from '../src/provider/in-memory-provider/variant-not-found-error';
+
+describe(InMemoryProvider, () => {
+  describe('initialize', () => {
+    it('Should have provider status as NOT_READY after instantiation and emit READY and have READY state after initialuzation', async () => {
+      const booleanFlagSpec = {
+        'a-boolean-flag': {
+          variants: {
+            on: true,
+            off: false,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+        },
+      };
+      const provider = new InMemoryProvider(booleanFlagSpec);
+      expect(provider.status).toBe(ProviderStatus.NOT_READY);
+
+      await provider.initialize();
+      expect(provider.status).toBe(ProviderStatus.READY);
+
+    });
+
+    it('Should have provider status as ERROR after instantiation, emit ERROR and have ERROR state if initialization throws', async () => {
+      const throwingFlagSpec: FlagConfiguration = {
+        'a-boolean-flag': {
+          variants: {
+            on: true,
+            off: false,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+          contextEvaluator: () => { throw new GeneralError('context eval error'); },
+        },
+      };
+      const provider = new InMemoryProvider(throwingFlagSpec);
+
+      expect(provider.status).toBe(ProviderStatus.NOT_READY);
+      const someContext = {};
+
+      await expect(provider.initialize(someContext)).rejects.toThrow();
+      expect(provider.status).toBe(ProviderStatus.ERROR);
+    });
+  });
+
+  describe('boolean flags', () => {
+    const provider = new InMemoryProvider({});
+    it('resolves to default variant with reason static', async () => {
+      const booleanFlagSpec = {
+        'a-boolean-flag': {
+          variants: {
+            on: true,
+            off: false,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+        },
+      };
+      await provider.putConfiguration(booleanFlagSpec);
+
+      const resolution = provider.resolveBooleanEvaluation('a-boolean-flag', true);
+
+      expect(resolution).toEqual({ value: true, reason: StandardResolutionReasons.STATIC, variant: 'on' });
+    });
+
+    it('throws FlagNotFound if flag does not exist', async () => {
+      const booleanFlagSpec = {
+        'a-boolean-flag': {
+          variants: {
+            on: true,
+            off: false,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+        },
+      };
+      await provider.putConfiguration(booleanFlagSpec);
+
+      expect(() => provider.resolveBooleanEvaluation('another-boolean-flag', false)).toThrow();
+    });
+
+    it('resolves to default value with reason disabled if flag is disabled', async () => {
+      const booleanFlagDisabledSpec = {
+        'a-boolean-flag': {
+          variants: {
+            on: true,
+            off: false,
+          },
+          disabled: true,
+          defaultVariant: 'on',
+        },
+      };
+      await provider.putConfiguration(booleanFlagDisabledSpec);
+
+      const resolution = provider.resolveBooleanEvaluation('a-boolean-flag', false);
+
+      expect(resolution).toEqual({ value: false, reason: StandardResolutionReasons.DISABLED });
+    });
+
+    it('throws VariantNotFoundError if variant does not exist', async () => {
+      const booleanFlagSpec = {
+        'a-boolean-flag': {
+          variants: {
+            on: true,
+            off: false,
+          },
+          disabled: false,
+          defaultVariant: 'dummy',
+        },
+      };
+      await provider.putConfiguration(booleanFlagSpec);
+
+      expect(() => provider.resolveBooleanEvaluation('a-boolean-flag', false)).toThrow(VariantNotFoundError);
+    });
+
+    it('throws TypeMismatchError if variant type does not match with accessors', async () => {
+      const booleanFlagSpec = {
+        'a-boolean-flag': {
+          variants: {
+            on: 'yes',
+            off: 'no',
+          },
+          disabled: false,
+          defaultVariant: 'on',
+        },
+      };
+      await provider.putConfiguration(booleanFlagSpec);
+
+      expect(() => provider.resolveBooleanEvaluation('a-boolean-flag', false)).toThrow(TypeMismatchError);
+    });
+
+    it('resolves to variant value with reason target match if context is provided and flag spec has context evaluator', async () => {
+      const booleanFlagCtxSpec = {
+        'a-boolean-flag': {
+          variants: {
+            on: true,
+            off: false,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+          contextEvaluator: () => 'off',
+        },
+      };
+      const dummyContext = {};
+      await provider.putConfiguration(booleanFlagCtxSpec);
+
+      const resolution = provider.resolveBooleanEvaluation('a-boolean-flag', true, dummyContext);
+
+      expect(resolution).toEqual({ value: false, reason: StandardResolutionReasons.TARGETING_MATCH, variant: 'off' });
+    });
+  });
+
+  describe('string flags', () => {
+    const provider = new InMemoryProvider({});
+    const itsDefault = "it's deafault";
+    const itsOn = "it's on";
+    const itsOff = "it's off";
+    it('resolves to default variant with reason static', async () => {
+      const stringFlagSpec = {
+        'a-string-flag': {
+          variants: {
+            on: itsOn,
+            off: itsOff,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+        },
+      };
+      await provider.putConfiguration(stringFlagSpec);
+
+      const resolution = provider.resolveStringEvaluation('a-string-flag', itsDefault);
+
+      expect(resolution).toEqual({ value: itsOn, reason: StandardResolutionReasons.STATIC, variant: 'on' });
+    });
+
+    it('throws FlagNotFound if flag does not exist', async () => {
+      const StringFlagSpec = {
+        'a-string-flag': {
+          variants: {
+            on: true,
+            off: false,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+        },
+      };
+
+      await provider.putConfiguration(StringFlagSpec);
+
+      expect(() => provider.resolveStringEvaluation('another-string-flag', itsDefault)).toThrow(FlagNotFoundError);
+    });
+
+    it('resolves to default value with reason disabled if flag is disabled', async () => {
+      const StringFlagDisabledSpec = {
+        'a-string-flag': {
+          variants: {
+            on: itsOn,
+            off: itsOff,
+          },
+          disabled: true,
+          defaultVariant: 'on',
+        },
+      };
+      await provider.putConfiguration(StringFlagDisabledSpec);
+
+      const resolution = provider.resolveStringEvaluation('a-string-flag', itsDefault);
+
+      expect(resolution).toEqual({ value: itsDefault, reason: StandardResolutionReasons.DISABLED });
+    });
+
+    it('throws VariantNotFoundError if variant does not exist', async () => {
+      const StringFlagSpec = {
+        'a-string-flag': {
+          variants: {
+            on: itsOn,
+            off: itsOff,
+          },
+          disabled: false,
+          defaultVariant: 'dummy',
+        },
+      };
+      await provider.putConfiguration(StringFlagSpec);
+
+      expect(() => provider.resolveStringEvaluation('a-string-flag', itsDefault)).toThrow(VariantNotFoundError);
+    });
+
+    it('throws TypeMismatchError if variant does not match with accessor method type', async () => {
+      const StringFlagSpec = {
+        'a-string-flag': {
+          variants: {
+            on: true,
+            off: false,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+        },
+      };
+
+      await provider.putConfiguration(StringFlagSpec);
+
+      expect(() => provider.resolveStringEvaluation('a-string-flag', itsDefault)).toThrow(TypeMismatchError);
+    });
+
+    it('resolves to variant value with reason target match if context is provided and flag spec has context evaluator', async () => {
+      const StringFlagCtxSpec = {
+        'a-string-flag': {
+          variants: {
+            on: itsOn,
+            off: itsOff,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+          contextEvaluator: () => 'off',
+        },
+      };
+      const dummyContext = {};
+      await provider.putConfiguration(StringFlagCtxSpec);
+
+      const resolution = provider.resolveStringEvaluation('a-string-flag', itsDefault, dummyContext);
+
+      expect(resolution).toEqual({ value: itsOff, reason: StandardResolutionReasons.TARGETING_MATCH, variant: 'off' });
+    });
+  });
+
+  describe('number flags', () => {
+    const provider = new InMemoryProvider({});
+    const defaultNumber = 42;
+    const onNumber = -528;
+    const offNumber = 0;
+    it('resolves to default variant with reason static', async () => {
+      const numberFlagSpec = {
+        'a-number-flag': {
+          variants: {
+            on: onNumber,
+            off: offNumber,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+        },
+      };
+      await provider.putConfiguration(numberFlagSpec);
+
+      const resolution = provider.resolveNumberEvaluation('a-number-flag', defaultNumber);
+
+      expect(resolution).toEqual({ value: onNumber, reason: StandardResolutionReasons.STATIC, variant: 'on' });
+    });
+
+    it('throws FlagNotFound if flag does not exist', async () => {
+      const numberFlagSpec = {
+        'a-number-flag': {
+          variants: {
+            on: onNumber,
+            off: offNumber,
+          },
+          disabled: false,
+          defaultVariant: 'dummy',
+        },
+      };
+      await provider.putConfiguration(numberFlagSpec);
+
+      expect(() => provider.resolveNumberEvaluation('another-number-flag', defaultNumber)).toThrow(FlagNotFoundError);
+    });
+
+    it('resolves to default value with reason disabled if flag is disabled', async () => {
+      const numberFlagDisabledSpec = {
+        'a-number-flag': {
+          variants: {
+            on: onNumber,
+            off: offNumber,
+          },
+          disabled: true,
+          defaultVariant: 'on',
+        },
+      };
+      await provider.putConfiguration(numberFlagDisabledSpec);
+
+      const resolution = provider.resolveNumberEvaluation('a-number-flag', defaultNumber);
+
+      expect(resolution).toEqual({ value: defaultNumber, reason: StandardResolutionReasons.DISABLED });
+    });
+
+    it('throws VariantNotFoundError if variant does not exist', async () => {
+      const numberFlagSpec = {
+        'a-number-flag': {
+          variants: {
+            on: onNumber,
+            off: offNumber,
+          },
+          disabled: false,
+          defaultVariant: 'dummy',
+        },
+      };
+      await provider.putConfiguration(numberFlagSpec);
+
+      expect(() => provider.resolveNumberEvaluation('a-number-flag', defaultNumber)).toThrow(VariantNotFoundError);
+    });
+
+    it('throws TypeMismatchError if variant does not match with accessor method type', async () => {
+      const numberFlagSpec = {
+        'a-number-flag': {
+          variants: {
+            on: true,
+            off: false,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+        },
+      };
+
+      await provider.putConfiguration(numberFlagSpec);
+
+      expect(() => provider.resolveNumberEvaluation('a-number-flag', defaultNumber)).toThrow(TypeMismatchError);
+    });
+
+    it('resolves to variant value with reason target match if context is provided and flag spec has context evaluator', async () => {
+      const numberFlagCtxSpec = {
+        'a-number-flag': {
+          variants: {
+            on: onNumber,
+            off: offNumber,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+          contextEvaluator: () => 'off',
+        },
+      };
+      const dummyContext = {};
+      await provider.putConfiguration(numberFlagCtxSpec);
+
+      const resolution = provider.resolveNumberEvaluation('a-number-flag', defaultNumber, dummyContext);
+
+      expect(resolution).toEqual({
+        value: offNumber,
+        reason: StandardResolutionReasons.TARGETING_MATCH,
+        variant: 'off',
+      });
+    });
+  });
+
+  describe('Object flags', () => {
+    const provider = new InMemoryProvider({});
+    const defaultObject = { someKey: 'default' };
+    const onObject = { someKey: 'on' };
+    const offObject = { someKey: 'off' };
+    it('resolves to default variant with reason static', async () => {
+      const ObjectFlagSpec = {
+        'a-object-flag': {
+          variants: {
+            on: onObject,
+            off: offObject,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+        },
+      };
+      await provider.putConfiguration(ObjectFlagSpec);
+
+      const resolution = provider.resolveObjectEvaluation('a-object-flag', defaultObject);
+
+      expect(resolution).toEqual({ value: onObject, reason: StandardResolutionReasons.STATIC, variant: 'on' });
+    });
+
+    it('throws FlagNotFound if flag does not exist', async () => {
+      const ObjectFlagSpec = {
+        'a-Object-flag': {
+          variants: {
+            on: onObject,
+            off: offObject,
+          },
+          disabled: false,
+          defaultVariant: 'dummy',
+        },
+      };
+      await provider.putConfiguration(ObjectFlagSpec);
+
+      expect(() => provider.resolveObjectEvaluation('another-number-flag', defaultObject)).toThrow(FlagNotFoundError);
+    });
+
+    it('resolves to default value with reason disabled if flag is disabled', async () => {
+      const ObjectFlagDisabledSpec = {
+        'a-object-flag': {
+          variants: {
+            on: onObject,
+            off: offObject,
+          },
+          disabled: true,
+          defaultVariant: 'on',
+        },
+      };
+      await provider.putConfiguration(ObjectFlagDisabledSpec);
+
+      const resolution = provider.resolveObjectEvaluation('a-object-flag', defaultObject);
+
+      expect(resolution).toEqual({ value: defaultObject, reason: StandardResolutionReasons.DISABLED });
+    });
+
+    it('throws VariantNotFoundError if variant does not exist', async () => {
+      const ObjectFlagSpec = {
+        'a-Object-flag': {
+          variants: {
+            on: onObject,
+            off: offObject,
+          },
+          disabled: false,
+          defaultVariant: 'dummy',
+        },
+      };
+      await provider.putConfiguration(ObjectFlagSpec);
+
+      expect(() => provider.resolveObjectEvaluation('a-Object-flag', defaultObject)).toThrow(VariantNotFoundError);
+    });
+
+    it('throws TypeMismatchError if variant does not match with accessor method type', async () => {
+      const ObjectFlagSpec = {
+        'a-object-flag': {
+          variants: {
+            on: true,
+            off: false,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+        },
+      };
+
+      await provider.putConfiguration(ObjectFlagSpec);
+
+      expect(() => provider.resolveObjectEvaluation('a-object-flag', defaultObject)).toThrow(TypeMismatchError);
+    });
+
+    it('resolves to variant value with reason target match if context is provided and flag spec has context evaluator', async () => {
+      const ObjectFlagCtxSpec = {
+        'a-object-flag': {
+          variants: {
+            on: onObject,
+            off: offObject,
+          },
+          disabled: false,
+          defaultVariant: 'on',
+          contextEvaluator: () => 'off',
+        },
+      };
+      const dummyContext = {};
+      await provider.putConfiguration(ObjectFlagCtxSpec);
+
+      const resolution = provider.resolveObjectEvaluation('a-object-flag', defaultObject, dummyContext);
+
+      expect(resolution).toEqual({
+        value: offObject,
+        reason: StandardResolutionReasons.TARGETING_MATCH,
+        variant: 'off',
+      });
+    });
+  });
+
+  describe('events', () => {
+    it('emits provider changed event, ready event and has READY status', async () => {
+      const flagsSpec = {
+        'some-flag': {
+          variants: {
+            on: 'initial-value',
+          },
+          defaultVariant: 'on',
+          disabled: false,
+        },
+      };
+      const provider = new InMemoryProvider(flagsSpec);
+
+      const configChangedSpy = jest.fn();
+      provider.events.addHandler(ProviderEvents.ConfigurationChanged, configChangedSpy);
+
+      const readySpy = jest.fn();
+      provider.events.addHandler(ProviderEvents.Ready, readySpy);
+
+      const newFlagSpec = {
+        'some-flag': {
+          variants: {
+            off: 'some-other-value',
+          },
+          defaultVariant: 'off',
+          disabled: false,
+        },
+      };
+      await provider.putConfiguration(newFlagSpec);
+
+      expect(configChangedSpy).toHaveBeenCalledWith({ flagsChanged: ['some-flag'] });
+      expect(readySpy).toHaveBeenCalled();
+      expect(provider.status).toBe(ProviderStatus.READY);
+    });
+  });
+
+  describe('Flags configuration', () => {
+    it('reflects changes in flag configuration', async () => {
+      const provider = new InMemoryProvider({
+        'some-flag': {
+          variants: {
+            on: 'initial-value',
+          },
+          defaultVariant: 'on',
+          disabled: false,
+        },
+      });
+
+      await provider.initialize();
+
+      const firstResolution = provider.resolveStringEvaluation('some-flag', 'deafaultFirstResolution');
+
+      expect(firstResolution).toEqual({
+        value: 'initial-value',
+        reason: StandardResolutionReasons.STATIC,
+        variant: 'on',
+      });
+
+      await provider.putConfiguration({
+        'some-flag': {
+          variants: {
+            on: 'new-value',
+          },
+          defaultVariant: 'on',
+          disabled: false,
+        },
+      });
+
+      const secondResolution = provider.resolveStringEvaluation('some-flag', 'defaultSecondResolution');
+
+      expect(secondResolution).toEqual({ value: 'new-value', reason: StandardResolutionReasons.STATIC, variant: 'on' });
+    });
+
+    it('does not let you change values with the configuration passed by reference', async () => {
+      const flagsSpec = {
+        'some-flag': {
+          variants: {
+            on: 'initial-value',
+          },
+          defaultVariant: 'on',
+          disabled: false,
+        },
+      };
+
+      const substituteSpec = {
+        variants: {
+          on: 'some-other-value',
+        },
+        defaultVariant: 'on',
+        disabled: false,
+      };
+
+      const provider = new InMemoryProvider(flagsSpec);
+
+      await provider.initialize();
+
+      // I passed configuration by reference, so maybe I can mess
+      // with it behind the providers back!
+      flagsSpec['some-flag'] = substituteSpec;
+
+      const resolution = provider.resolveStringEvaluation('some-flag', 'default value');
+      expect(resolution).toEqual({ value: 'initial-value', reason: StandardResolutionReasons.STATIC, variant: 'on' });
+    });
+  });
+});