Skip to content

Commit 5bc08ea

Browse files
committed
Introduce testing suite to ENSDb SDK
Covers `EnsDbReader` and `EnsDbWriter`.
1 parent 8edabdd commit 5bc08ea

5 files changed

Lines changed: 386 additions & 0 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {
2+
type BlockRef,
3+
ChainIndexingStatusIds,
4+
CrossChainIndexingStrategyIds,
5+
type EnsIndexerPublicConfig,
6+
OmnichainIndexingStatusIds,
7+
PluginName,
8+
RangeTypeIds,
9+
type SerializedCrossChainIndexingStatusSnapshot,
10+
} from "@ensnode/ensnode-sdk";
11+
12+
export const earlierBlockRef = {
13+
timestamp: 1672531199,
14+
number: 1024,
15+
} as const satisfies BlockRef;
16+
17+
export const laterBlockRef = {
18+
timestamp: 1672531200,
19+
number: 1025,
20+
} as const satisfies BlockRef;
21+
22+
export const ensDbUrl = "postgres://user:pass@localhost:5432/ensdb";
23+
24+
export const ensIndexerSchemaName = "ensindexer_0";
25+
26+
export const publicConfig = {
27+
databaseSchemaName: ensIndexerSchemaName,
28+
ensRainbowPublicConfig: {
29+
version: "0.32.0",
30+
labelSet: {
31+
labelSetId: "subgraph",
32+
highestLabelSetVersion: 0,
33+
},
34+
recordsCount: 100,
35+
},
36+
labelSet: {
37+
labelSetId: "subgraph",
38+
labelSetVersion: 0,
39+
},
40+
indexedChainIds: new Set([1]),
41+
isSubgraphCompatible: true,
42+
namespace: "mainnet",
43+
plugins: [PluginName.Subgraph],
44+
versionInfo: {
45+
nodejs: "v22.10.12",
46+
ponder: "0.11.25",
47+
ensDb: "0.32.0",
48+
ensIndexer: "0.32.0",
49+
ensNormalize: "1.11.1",
50+
},
51+
} satisfies EnsIndexerPublicConfig;
52+
53+
export const serializedSnapshot = {
54+
strategy: CrossChainIndexingStrategyIds.Omnichain,
55+
slowestChainIndexingCursor: earlierBlockRef.timestamp,
56+
snapshotTime: earlierBlockRef.timestamp + 20,
57+
omnichainSnapshot: {
58+
omnichainStatus: OmnichainIndexingStatusIds.Following,
59+
chains: {
60+
"1": {
61+
chainStatus: ChainIndexingStatusIds.Following,
62+
config: {
63+
rangeType: RangeTypeIds.LeftBounded,
64+
startBlock: earlierBlockRef,
65+
},
66+
latestIndexedBlock: earlierBlockRef,
67+
latestKnownBlock: laterBlockRef,
68+
},
69+
},
70+
omnichainIndexingCursor: earlierBlockRef.timestamp,
71+
},
72+
} satisfies SerializedCrossChainIndexingStatusSnapshot;
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
import {
4+
deserializeCrossChainIndexingStatusSnapshot,
5+
serializeEnsIndexerPublicConfig,
6+
} from "@ensnode/ensnode-sdk";
7+
8+
import * as ensNodeSchema from "../ensnode";
9+
import { buildEnsDbDrizzleClient } from "../lib/drizzle";
10+
import * as ensDbClientMock from "./ensdb-client.mock";
11+
import { EnsDbReader } from "./ensdb-reader";
12+
13+
// Mock the config module to prevent it from trying to load actual environment variables during tests
14+
vi.mock("@/config", () => ({ default: {} }));
15+
16+
// Mock the buildEnsDbDrizzleClient function to return a mock database instance
17+
vi.mock("../lib/drizzle", () => ({ buildEnsDbDrizzleClient: vi.fn() }));
18+
19+
describe("EnsDbReader", () => {
20+
// Mock database query results and methods
21+
const selectResult = { current: [] as Array<{ value: unknown }> };
22+
const whereMock = vi.fn(async () => selectResult.current);
23+
const fromMock = vi.fn(() => ({ where: whereMock }));
24+
const selectMock = vi.fn(() => ({ from: fromMock }));
25+
const onConflictDoUpdateMock = vi.fn(async () => undefined);
26+
const valuesMock = vi.fn(() => ({ onConflictDoUpdate: onConflictDoUpdateMock }));
27+
const insertMock = vi.fn(() => ({ values: valuesMock }));
28+
const executeMock = vi.fn(async () => undefined);
29+
const txMock = { insert: insertMock, execute: executeMock };
30+
const transactionMock = vi.fn(async (callback: (tx: typeof txMock) => Promise<void>) =>
31+
callback(txMock),
32+
);
33+
const dbMock = { select: selectMock, insert: insertMock, transaction: transactionMock };
34+
35+
beforeEach(() => {
36+
selectResult.current = [];
37+
whereMock.mockClear();
38+
fromMock.mockClear();
39+
selectMock.mockClear();
40+
onConflictDoUpdateMock.mockClear();
41+
valuesMock.mockClear();
42+
insertMock.mockClear();
43+
executeMock.mockClear();
44+
transactionMock.mockClear();
45+
vi.mocked(buildEnsDbDrizzleClient).mockReturnValue(
46+
dbMock as unknown as ReturnType<typeof buildEnsDbDrizzleClient>,
47+
);
48+
});
49+
50+
describe("getEnsDbVersion", () => {
51+
it("returns undefined when no record exists", async () => {
52+
// arrange
53+
const client = new EnsDbReader(
54+
ensDbClientMock.ensDbUrl,
55+
ensDbClientMock.ensIndexerSchemaName,
56+
);
57+
58+
// act & assert
59+
await expect(client.getEnsDbVersion()).resolves.toBeUndefined();
60+
61+
expect(selectMock).toHaveBeenCalledTimes(1);
62+
expect(fromMock).toHaveBeenCalledWith(ensNodeSchema.metadata);
63+
});
64+
65+
it("returns value when one record exists", async () => {
66+
// arrange
67+
selectResult.current = [{ value: "0.1.0" }];
68+
69+
const client = new EnsDbReader(
70+
ensDbClientMock.ensDbUrl,
71+
ensDbClientMock.ensIndexerSchemaName,
72+
);
73+
74+
// act & assert
75+
await expect(client.getEnsDbVersion()).resolves.toBe("0.1.0");
76+
});
77+
78+
// This scenario should be impossible due to the primary key constraint on
79+
// the 'key' column of 'ensnode_metadata' table.
80+
it("throws when multiple records exist", async () => {
81+
// arrange
82+
selectResult.current = [{ value: "0.1.0" }, { value: "0.1.1" }];
83+
84+
const client = new EnsDbReader(
85+
ensDbClientMock.ensDbUrl,
86+
ensDbClientMock.ensIndexerSchemaName,
87+
);
88+
89+
// act & assert
90+
await expect(client.getEnsDbVersion()).rejects.toThrowError(/ensdb_version/i);
91+
});
92+
});
93+
94+
describe("getEnsIndexerPublicConfig", () => {
95+
it("returns undefined when no record exists", async () => {
96+
// arrange
97+
const client = new EnsDbReader(
98+
ensDbClientMock.ensDbUrl,
99+
ensDbClientMock.ensIndexerSchemaName,
100+
);
101+
102+
// act & assert
103+
await expect(client.getEnsIndexerPublicConfig()).resolves.toBeUndefined();
104+
});
105+
106+
it("deserializes the stored config", async () => {
107+
// arrange
108+
const serializedConfig = serializeEnsIndexerPublicConfig(ensDbClientMock.publicConfig);
109+
selectResult.current = [{ value: serializedConfig }];
110+
111+
const client = new EnsDbReader(
112+
ensDbClientMock.ensDbUrl,
113+
ensDbClientMock.ensIndexerSchemaName,
114+
);
115+
116+
// act & assert
117+
await expect(client.getEnsIndexerPublicConfig()).resolves.toStrictEqual(
118+
ensDbClientMock.publicConfig,
119+
);
120+
});
121+
});
122+
123+
describe("getIndexingStatusSnapshot", () => {
124+
it("deserializes the stored indexing status snapshot", async () => {
125+
// arrange
126+
selectResult.current = [{ value: ensDbClientMock.serializedSnapshot }];
127+
128+
const client = new EnsDbReader(
129+
ensDbClientMock.ensDbUrl,
130+
ensDbClientMock.ensIndexerSchemaName,
131+
);
132+
const expected = deserializeCrossChainIndexingStatusSnapshot(
133+
ensDbClientMock.serializedSnapshot,
134+
);
135+
136+
// act & assert
137+
await expect(client.getIndexingStatusSnapshot()).resolves.toStrictEqual(expected);
138+
});
139+
});
140+
});
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { migrate } from "drizzle-orm/node-postgres/migrator";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
4+
import {
5+
deserializeCrossChainIndexingStatusSnapshot,
6+
serializeCrossChainIndexingStatusSnapshot,
7+
serializeEnsIndexerPublicConfig,
8+
} from "@ensnode/ensnode-sdk";
9+
10+
import * as ensNodeSchema from "../ensnode";
11+
import { buildEnsDbDrizzleClient } from "../lib/drizzle";
12+
import * as ensDbClientMock from "./ensdb-client.mock";
13+
import { EnsDbWriter } from "./ensdb-writer";
14+
import { EnsNodeMetadataKeys } from "./ensnode-metadata";
15+
16+
// Mock the config module to prevent it from trying to load actual environment variables during tests
17+
vi.mock("@/config", () => ({ default: {} }));
18+
19+
// Mock the buildEnsDbDrizzleClient function to return a mock database instance
20+
vi.mock("../lib/drizzle", () => ({ buildEnsDbDrizzleClient: vi.fn() }));
21+
22+
// Mock the drizzle-orm migrator
23+
vi.mock("drizzle-orm/node-postgres/migrator", () => ({ migrate: vi.fn() }));
24+
25+
describe("EnsDbWriter", () => {
26+
// Mock database query results and methods
27+
const selectResult = { current: [] as Array<{ value: unknown }> };
28+
const whereMock = vi.fn(async () => selectResult.current);
29+
const fromMock = vi.fn(() => ({ where: whereMock }));
30+
const selectMock = vi.fn(() => ({ from: fromMock }));
31+
const onConflictDoUpdateMock = vi.fn(async () => undefined);
32+
const valuesMock = vi.fn(() => ({ onConflictDoUpdate: onConflictDoUpdateMock }));
33+
const insertMock = vi.fn(() => ({ values: valuesMock }));
34+
const executeMock = vi.fn(async () => undefined);
35+
const txMock = { insert: insertMock, execute: executeMock };
36+
const transactionMock = vi.fn(async (callback: (tx: typeof txMock) => Promise<void>) =>
37+
callback(txMock),
38+
);
39+
const dbMock = { select: selectMock, insert: insertMock, transaction: transactionMock };
40+
41+
beforeEach(() => {
42+
selectResult.current = [];
43+
whereMock.mockClear();
44+
fromMock.mockClear();
45+
selectMock.mockClear();
46+
onConflictDoUpdateMock.mockClear();
47+
valuesMock.mockClear();
48+
insertMock.mockClear();
49+
executeMock.mockClear();
50+
transactionMock.mockClear();
51+
vi.mocked(migrate).mockClear();
52+
vi.mocked(buildEnsDbDrizzleClient).mockReturnValue(
53+
dbMock as unknown as ReturnType<typeof buildEnsDbDrizzleClient>,
54+
);
55+
});
56+
57+
describe("upsertEnsDbVersion", () => {
58+
it("writes the database version metadata", async () => {
59+
// arrange
60+
const client = new EnsDbWriter(
61+
ensDbClientMock.ensDbUrl,
62+
ensDbClientMock.ensIndexerSchemaName,
63+
);
64+
65+
// act
66+
await client.upsertEnsDbVersion("0.2.0");
67+
68+
// assert
69+
expect(insertMock).toHaveBeenCalledWith(ensNodeSchema.metadata);
70+
expect(valuesMock).toHaveBeenCalledWith({
71+
ensIndexerSchemaName: ensDbClientMock.ensIndexerSchemaName,
72+
key: EnsNodeMetadataKeys.EnsDbVersion,
73+
value: "0.2.0",
74+
});
75+
expect(onConflictDoUpdateMock).toHaveBeenCalledWith({
76+
target: [ensNodeSchema.metadata.ensIndexerSchemaName, ensNodeSchema.metadata.key],
77+
set: { value: "0.2.0" },
78+
});
79+
});
80+
});
81+
82+
describe("upsertEnsIndexerPublicConfig", () => {
83+
it("serializes and writes the public config", async () => {
84+
// arrange
85+
const client = new EnsDbWriter(
86+
ensDbClientMock.ensDbUrl,
87+
ensDbClientMock.ensIndexerSchemaName,
88+
);
89+
const expectedValue = serializeEnsIndexerPublicConfig(ensDbClientMock.publicConfig);
90+
91+
// act
92+
await client.upsertEnsIndexerPublicConfig(ensDbClientMock.publicConfig);
93+
94+
// assert
95+
expect(valuesMock).toHaveBeenCalledWith({
96+
ensIndexerSchemaName: ensDbClientMock.ensIndexerSchemaName,
97+
key: EnsNodeMetadataKeys.EnsIndexerPublicConfig,
98+
value: expectedValue,
99+
});
100+
});
101+
});
102+
103+
describe("upsertIndexingStatusSnapshot", () => {
104+
it("serializes and writes the indexing status snapshot", async () => {
105+
// arrange
106+
const client = new EnsDbWriter(
107+
ensDbClientMock.ensDbUrl,
108+
ensDbClientMock.ensIndexerSchemaName,
109+
);
110+
const snapshot = deserializeCrossChainIndexingStatusSnapshot(
111+
ensDbClientMock.serializedSnapshot,
112+
);
113+
const expectedValue = serializeCrossChainIndexingStatusSnapshot(snapshot);
114+
115+
// act
116+
await client.upsertIndexingStatusSnapshot(snapshot);
117+
118+
// assert
119+
expect(valuesMock).toHaveBeenCalledWith({
120+
ensIndexerSchemaName: ensDbClientMock.ensIndexerSchemaName,
121+
key: EnsNodeMetadataKeys.EnsIndexerIndexingStatus,
122+
value: expectedValue,
123+
});
124+
});
125+
});
126+
127+
describe("migrate", () => {
128+
it("calls drizzle-orm migrate with the correct parameters", async () => {
129+
// arrange
130+
const client = new EnsDbWriter(
131+
ensDbClientMock.ensDbUrl,
132+
ensDbClientMock.ensIndexerSchemaName,
133+
);
134+
const migrationsDirPath = "/path/to/migrations";
135+
136+
// act
137+
await client.migrate(migrationsDirPath);
138+
139+
// assert
140+
expect(vi.mocked(migrate)).toHaveBeenCalledWith(dbMock, {
141+
migrationsFolder: migrationsDirPath,
142+
migrationsSchema: "ensnode",
143+
});
144+
});
145+
146+
it("propagates errors from the migrate function", async () => {
147+
// arrange
148+
const client = new EnsDbWriter(
149+
ensDbClientMock.ensDbUrl,
150+
ensDbClientMock.ensIndexerSchemaName,
151+
);
152+
const migrationsDirPath = "/path/to/migrations";
153+
const error = new Error("Migration failed");
154+
vi.mocked(migrate).mockRejectedValueOnce(error);
155+
156+
// act & assert
157+
await expect(client.migrate(migrationsDirPath)).rejects.toThrow("Migration failed");
158+
});
159+
});
160+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { resolve } from "node:path";
2+
3+
import { defineProject } from "vitest/config";
4+
5+
export default defineProject({
6+
resolve: {
7+
alias: {
8+
"@": resolve(__dirname, "./src"),
9+
},
10+
},
11+
});

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)