import { describe, it, expect, vi, beforeEach } from "vitest";
import {
createMockMySQLAdapter,
createMockQueryResult,
createMockRequestContext,
} from "../../../../__tests__/mocks/index.js";
import type { MySQLAdapter } from "../../MySQLAdapter.js";
import { createReplicationResource } from "../replication.js";
describe("Replication Resource", () => {
let mockAdapter: ReturnType<typeof createMockMySQLAdapter>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockMySQLAdapter();
mockContext = createMockRequestContext();
});
it("should handle empty result sets gracefully", async () => {
// SHOW REPLICA STATUS (returns empty array)
mockAdapter.executeQuery.mockResolvedValueOnce(createMockQueryResult([]));
// Fallback: SHOW SLAVE STATUS (returns empty array)
mockAdapter.executeQuery.mockResolvedValueOnce(createMockQueryResult([]));
// SHOW BINARY LOG STATUS (returns empty array)
mockAdapter.executeQuery.mockResolvedValueOnce(createMockQueryResult([]));
// Fallback: SHOW MASTER STATUS (returns empty array)
mockAdapter.executeQuery.mockResolvedValueOnce(createMockQueryResult([]));
// GTID (fails or empty)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("No GTID"));
// SHOW REPLICAS (empty)
mockAdapter.executeQuery.mockResolvedValueOnce(createMockQueryResult([]));
const resource = createReplicationResource(
mockAdapter as unknown as MySQLAdapter,
);
const result = (await resource.handler(
"mysql://replication",
mockContext,
)) as any;
expect(result.role).toBe("standalone");
expect(result.source).toBeNull();
expect(result.replica).toBeNull();
});
it("should return source role info correctly", async () => {
// SHOW REPLICA STATUS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("Not found"));
// SHOW SLAVE STATUS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("Not found"));
// SHOW BINARY LOG STATUS
mockAdapter.executeQuery.mockResolvedValueOnce(
createMockQueryResult([
{
File: "bin.001",
Position: 100,
Binlog_Do_DB: "",
Binlog_Ignore_DB: "",
},
]),
);
// GTID
mockAdapter.executeQuery.mockResolvedValueOnce(createMockQueryResult([]));
// SHOW REPLICAS
mockAdapter.executeQuery.mockResolvedValueOnce(
createMockQueryResult([{ Host: "slave1" }]),
);
const resource = createReplicationResource(
mockAdapter as unknown as MySQLAdapter,
);
const result = (await resource.handler(
"mysql://replication",
mockContext,
)) as any;
expect(result.role).toBe("source");
expect(result.source).toBeDefined();
expect(result.connected_replicas).toHaveLength(1);
});
it("should return replica role info correctly", async () => {
// SHOW REPLICA STATUS
mockAdapter.executeQuery.mockResolvedValueOnce(
createMockQueryResult([
{
Source_Host: "master1",
Slave_IO_Running: "Yes",
Slave_SQL_Running: "Yes",
Seconds_Behind_Master: 0,
},
]),
);
// SHOW BINARY LOG STATUS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("Not source"));
// SHOW MASTER STATUS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("Not source"));
// GTID
mockAdapter.executeQuery.mockResolvedValueOnce(createMockQueryResult([]));
// SHOW REPLICAS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("Not source"));
// SHOW SLAVE HOSTS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("Not source"));
const resource = createReplicationResource(
mockAdapter as unknown as MySQLAdapter,
);
const result = (await resource.handler(
"mysql://replication",
mockContext,
)) as any;
expect(result.role).toBe("replica");
expect(result.replica).toBeDefined();
expect(result.replica.source_host).toBe("master1");
});
it("should handle replica-source role", async () => {
// SHOW REPLICA STATUS
mockAdapter.executeQuery.mockResolvedValueOnce(
createMockQueryResult([{ Source_Host: "master1" }]),
);
// SHOW BINARY LOG STATUS
mockAdapter.executeQuery.mockResolvedValueOnce(
createMockQueryResult([{ File: "bin.001" }]),
);
// GTID
mockAdapter.executeQuery.mockResolvedValueOnce(createMockQueryResult([]));
// SHOW REPLICAS
mockAdapter.executeQuery.mockResolvedValueOnce(createMockQueryResult([]));
const resource = createReplicationResource(
mockAdapter as unknown as MySQLAdapter,
);
const result = (await resource.handler(
"mysql://replication",
mockContext,
)) as any;
expect(result.role).toBe("replica-source");
expect(result.gtid).toEqual({});
});
it("should parse GTID information", async () => {
// SHOW REPLICA STATUS (fail - not a replica)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("Not a replica"));
// SHOW SLAVE STATUS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("Not a replica"));
// SHOW BINARY LOG STATUS (source)
mockAdapter.executeQuery.mockResolvedValueOnce(
createMockQueryResult([{ File: "bin.001" }]),
);
// GTID
mockAdapter.executeQuery.mockResolvedValueOnce(
createMockQueryResult([
{ Variable_name: "gtid_mode", Value: "ON" },
{ Variable_name: "gtid_executed", Value: "uuid:1-100" },
]),
);
// SHOW REPLICAS
mockAdapter.executeQuery.mockResolvedValueOnce(createMockQueryResult([]));
const resource = createReplicationResource(
mockAdapter as unknown as MySQLAdapter,
);
const result = (await resource.handler(
"mysql://replication",
mockContext,
)) as any;
expect(result.role).toBe("source");
expect(result.gtid).toEqual({
gtid_mode: "ON",
gtid_executed: "uuid:1-100",
});
});
it("should fallback to older syntax for replicas", async () => {
// SHOW REPLICA STATUS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("Syntax error"));
// SHOW SLAVE STATUS
mockAdapter.executeQuery.mockResolvedValueOnce(
createMockQueryResult([{ Master_Host: "master1" }]),
);
// SHOW BINARY LOG STATUS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("fail"));
// SHOW MASTER STATUS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("fail"));
// GTID
mockAdapter.executeQuery.mockResolvedValueOnce(createMockQueryResult([]));
// SHOW REPLICAS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("fail"));
// SHOW SLAVE HOSTS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("fail"));
const resource = createReplicationResource(
mockAdapter as unknown as MySQLAdapter,
);
const result = (await resource.handler(
"mysql://replication",
mockContext,
)) as any;
expect(result.replica).toBeDefined();
expect(result.replica.source_host).toBe("master1");
});
it("should fallback to older syntax for source", async () => {
// SHOW REPLICA STATUS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("fail"));
// SHOW SLAVE STATUS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("fail"));
// SHOW BINARY LOG STATUS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("fail"));
// SHOW MASTER STATUS
mockAdapter.executeQuery.mockResolvedValueOnce(
createMockQueryResult([{ File: "bin.001" }]),
);
// GTID
mockAdapter.executeQuery.mockResolvedValueOnce(createMockQueryResult([]));
// SHOW REPLICAS (fail)
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("fail"));
// SHOW SLAVE HOSTS
mockAdapter.executeQuery.mockResolvedValueOnce(
createMockQueryResult([{ Host: "slave1" }]),
);
const resource = createReplicationResource(
mockAdapter as unknown as MySQLAdapter,
);
const result = (await resource.handler(
"mysql://replication",
mockContext,
)) as any;
expect(result.role).toBe("source");
expect(result.connected_replicas).toHaveLength(1);
});
});