Skip to content

Commit

Permalink
Add tests for the sdk directory (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
atrakh authored Oct 25, 2024
1 parent 627109d commit ec181ab
Show file tree
Hide file tree
Showing 9 changed files with 507 additions and 11 deletions.
3 changes: 2 additions & 1 deletion src/component/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe("events", () => {

test("should store an event", async () => {
const t = convexTest(schema, modules);

await t.run(async (ctx) => {
const payload = JSON.stringify({ event: "test-event" });
await storeEvents(ctx, { sdkKey: "test-sdk-key", payloads: [payload] });
Expand Down Expand Up @@ -204,7 +205,7 @@ describe("events", () => {

await t.finishAllScheduledFunctions(vi.runAllTimers);

expect(sendEvents).toHaveBeenCalledTimes(1);
expect(sendEvents).toHaveBeenCalledOnce();
expect(sendEvents).toBeCalledWith(events, "test-sdk-key", undefined);

await t.run(async (ctx) => {
Expand Down
37 changes: 37 additions & 0 deletions src/sdk/EventProcessor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, test, vi } from "vitest";
import { EventProcessor } from "./EventProcessor";
import { convexTest } from "convex-test";
import schema from "../component/schema";
import { modules } from "../component/setup.test";
import { api } from "../component/_generated/api";
import { sendEvents } from "../sdk/EventProcessor";

describe("EventProcessor", () => {
vi.mock("../sdk/EventProcessor", async (importOriginal) => {
const original =
await importOriginal<typeof import("../sdk/EventProcessor")>();
return {
...original,
sendEvents: vi.fn(),
};
});

test("sendEvents should send events correctly", async () => {
vi.useFakeTimers();
const events = [{ payload: "event1" }, { payload: "event2" }];
const sdkKey = "test-sdk-key";

const t = convexTest(schema, modules);

await t.run(async (ctx) => {
// @ts-expect-error It's ok
const eventProcessor = new EventProcessor(api.events, ctx, sdkKey);

await eventProcessor.sendEvent(events[0]);
});

await t.finishAllScheduledFunctions(vi.runAllTimers);

expect(sendEvents).toHaveBeenCalledOnce();
});
});
12 changes: 5 additions & 7 deletions src/sdk/EventProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,10 @@ export class EventProcessor {
) {}

sendEvent(inputEvent: object) {
void (async () => {
await this.ctx.runMutation(this.eventStore.storeEvents, {
payloads: [JSON.stringify(inputEvent)],
sdkKey: this.sdkKey,
});
})();
return this.ctx.runMutation(this.eventStore.storeEvents, {
payloads: [JSON.stringify(inputEvent)],
sdkKey: this.sdkKey,
});
}

async flush() {
Expand All @@ -54,7 +52,7 @@ export const sendEvents = async (
const platform: Platform = {
info: createPlatformInfo(),
crypto: new ConvexCrypto(),
// @ts-expect-error We only alow fetch
// @ts-expect-error We only allow fetch
requests: { fetch },
};

Expand Down
178 changes: 178 additions & 0 deletions src/sdk/FeatureStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { describe, expect, test, vi } from "vitest";
import { FeatureStore } from "./FeatureStore";
import { convexTest } from "convex-test";
import schema from "../component/schema";
import { modules } from "../component/setup.test";
import { api } from "../component/_generated/api";
import { write } from "../component/store";
import * as store from "../component/store";

describe("FeatureStore", () => {
describe("get", () => {
test("invalid kind should throw an error", async () => {
const t = convexTest(schema, modules);
await t.run(async (ctx) => {
// @ts-expect-error It's ok
const featureStore = new FeatureStore(ctx, api.store, console);
const invalidKind = "invalidKind";

await expect(
featureStore.get({ namespace: invalidKind }, "someKey", vi.fn())
).rejects.toThrow(new Error(`Unsupported DataKind: ${invalidKind}`));
});
});

test.each(["features", "segments"])(
"get should return null when not initialized for kind %s",
async (namespace) => {
const t = convexTest(schema, modules);
await t.run(async (ctx) => {
// @ts-expect-error It's ok
const featureStore = new FeatureStore(ctx, api.store, console);
const k = {
namespace: namespace,
};
const key = "nonExistingKey";
const consoleSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});

await featureStore.get(k, key, (res) => {
expect(res).toBeNull();
});
expect(consoleSpy).toHaveBeenCalledWith(
"The LaunchDarkly data store has not been initialized. Is your integration configuration correct?"
);
});
}
);

test.each(["features", "segments"])(
"get should return cached value for kind %s",
async (kind) => {
const t = convexTest(schema, modules);
await t.run(async (ctx) => {
const config = { key: "existingKey", version: 1 };

await write(ctx, {
payload: JSON.stringify({
flags: {
[config.key]: config,
},
segments: {
[config.key]: config,
},
}),
});

// @ts-expect-error It's ok
const featureStore = new FeatureStore(ctx, api.store, console);
const k = { namespace: kind };
const key = config.key;

const spy = spyOnQuery(store, "get");

await featureStore.get(k, key, (res) => {
expect(res).toEqual(config);
});

expect(spy).toHaveBeenCalledOnce();

await featureStore.get(k, key, (res) => {
expect(res).toEqual(config);
});

expect(spy).toHaveBeenCalledOnce(); // should not call get again
});
}
);
});

describe("all", () => {
test("invalid kind should throw an error", async () => {
const t = convexTest(schema, modules);
await t.run(async (ctx) => {
// @ts-expect-error It's ok
const featureStore = new FeatureStore(ctx, api.store, console);
const invalidKind = "invalidKind";

await expect(
featureStore.all({ namespace: invalidKind }, vi.fn())
).rejects.toThrow(new Error(`Unsupported DataKind: ${invalidKind}`));
});
});

test.each(["features", "segments"])(
"all should return empty object when not initialized for kind %s",
async (kind) => {
const t = convexTest(schema, modules);
await t.run(async (ctx) => {
// @ts-expect-error It's ok
const featureStore = new FeatureStore(ctx, api.store, console);
const k = { namespace: kind };

await featureStore.all(k, (res) => {
expect(res).toEqual({});
});
});
}
);

test.each(["features", "segments"])(
"all should return cached values for kind %s",
async (kind) => {
const t = convexTest(schema, modules);
await t.run(async (ctx) => {
const config = { key: "existingKey", version: 1 };

await write(ctx, {
payload: JSON.stringify({
flags: {
[config.key]: config,
},
segments: {
[config.key]: config,
},
}),
});

// @ts-expect-error It's ok
const featureStore = new FeatureStore(ctx, api.store, console);
const k = { namespace: kind };

const getAllSpy = spyOnQuery(store, "getAll");

await featureStore.all(k, (res) => {
expect(res).toEqual({ [config.key]: config });
});

expect(getAllSpy).toHaveBeenCalledOnce();

await featureStore.all(k, (res) => {
expect(res).toEqual({ [config.key]: config });
});

expect(getAllSpy).toHaveBeenCalledOnce(); // should not call getAll again

const getSpy = spyOnQuery(store, "get");

await featureStore.get(k, config.key, (res) => {
expect(res).toEqual(config);
});

expect(getSpy).toHaveBeenCalledTimes(0); // should not call get because getAll was already called
});
}
);
});
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function spyOnQuery(api: any, method: string) {
const spy = vi.spyOn(api, method);
// @ts-expect-error It's a query ;)
spy.isQuery = true;
// @ts-expect-error Ignore validators
spy.exportArgs = () => "{}";
return spy;
}
3 changes: 3 additions & 0 deletions src/sdk/FeatureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export class FeatureStore implements LDFeatureStore {
this.logger.error(
"The LaunchDarkly data store has not been initialized. Is your integration configuration correct?"
);
callback(null);
return;
}

if (this.cache[kindKey][dataKey]) {
Expand Down Expand Up @@ -88,6 +90,7 @@ export class FeatureStore implements LDFeatureStore {
this.logger.error(
"The LaunchDarkly data store has not been initialized. Is your integration configuration correct?"
);
callback({});
}

if (kindKey === "flags" ? this.gotAllFlags : this.gotAllSegments) {
Expand Down
85 changes: 85 additions & 0 deletions src/sdk/LaunchDarkly.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, test, vi } from "vitest";
import { LaunchDarkly } from "./LaunchDarkly";
import { api } from "../component/_generated/api";
import { EventProcessor } from "./EventProcessor";

describe("LaunchDarkly", () => {
// The LaunchDarkly internals sometimes call functions that use setTimeout and setInterval.
// Ensure that our configuration is set up such that they are not called.
test("initializing class does not crash and setTimeout and setInterval are not called", async () => {
const setTimeoutSpy = vi.spyOn(global, "setTimeout");
const setIntervalSpy = vi.spyOn(global, "setInterval");
new LaunchDarkly(
// @ts-expect-error It's ok
api,
{},
{
LAUNCHDARKLY_SDK_KEY: "test-key",
}
);

expect(setTimeoutSpy).not.toHaveBeenCalled();
expect(setIntervalSpy).not.toHaveBeenCalled();
});

test("should throw an error if LAUNCHDARKLY_SDK_KEY is not provided", async () => {
await expect(
// @ts-expect-error It's ok
() => new LaunchDarkly(api, {}, {})
).toThrow(new Error("LAUNCHDARKLY_SDK_KEY is required"));
});

test("should not throw an error if the env var is set", () => {
vi.stubEnv("LAUNCHDARKLY_SDK_KEY", "test-key");

expect(() => {
// @ts-expect-error It's ok
new LaunchDarkly(api, {}, {});
}).not.toThrow();

vi.unstubAllEnvs();
});

test("should not configure the EventProcessor when in a query", () => {
const ld = new LaunchDarkly(
// @ts-expect-error It's ok
api,
{},
{
LAUNCHDARKLY_SDK_KEY: "test-key",
}
);

// @ts-expect-error We are testing internal state
expect(ld.eventProcessor).not.toBeInstanceOf(EventProcessor);
});

test("should configure the EventProcessor when in a mutation", () => {
const ld = new LaunchDarkly(
// @ts-expect-error It's ok
api,
{ runMutation: () => {} },
{
LAUNCHDARKLY_SDK_KEY: "test-key",
}
);

// @ts-expect-error We are testing internal state
expect(ld.eventProcessor).toBeInstanceOf(EventProcessor);
});

test("should not configure the EventProcessor when sendEvents is false", () => {
const ld = new LaunchDarkly(
// @ts-expect-error It's ok
api,
{ runMutation: () => {} },
{
LAUNCHDARKLY_SDK_KEY: "test-key",
sendEvents: false,
}
);

// @ts-expect-error We are testing internal state
expect(ld.eventProcessor).not.toBeInstanceOf(EventProcessor);
});
});
5 changes: 2 additions & 3 deletions src/sdk/LaunchDarkly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export class LaunchDarkly extends LDClientImpl {
featureStore,
...createOptions(logger),
...(options || {}),
// Even though the SDK can send events with our own implementation, we need
// to set this value to false so the super() constructor does not call EventProcessor.start() which calls setInterval.
sendEvents: false,
};

Expand Down Expand Up @@ -62,9 +64,6 @@ export class LaunchDarkly extends LDClientImpl {
}

export const createOptions = (logger: LDLogger): LDOptions => ({
// Even though the SDK can send events with our own implementation, we need
// to set this value to false so the super() constructor does not call EventProcessor.start() which calls setInterval.
sendEvents: false,
diagnosticOptOut: true,
useLdd: false,

Expand Down
Loading

0 comments on commit ec181ab

Please sign in to comment.