diff --git a/.gitignore b/.gitignore index 9639d1e..403fae3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules *.env dist +/.vscode diff --git a/test/cases/BaseDeployMissionUnit.test.ts b/test/cases/BaseDeployMissionUnit.test.ts new file mode 100644 index 0000000..6c364cb --- /dev/null +++ b/test/cases/BaseDeployMissionUnit.test.ts @@ -0,0 +1,200 @@ +import assert from "node:assert"; +import { + DBVersioner, + DeployCampaign, + HardhatDeployer, + IContractState, + IDeployCampaignConfig, + IHardhatBase, + ISignerBase, + MongoDBAdapter, +} from "../../src"; +import { HardhatMock } from "../mocks/hardhat"; +import { loggerMock } from "../mocks/logger"; +import { testMissions } from "../mocks/missions"; +import { MongoClientMock } from "../mocks/mongo"; +import { assertIsContract } from "../helpers/isContractCheck"; + + +describe("Base deploy mission", () => { + + let campaign : DeployCampaign, IContractState>; + let hardhatMock : HardhatMock; + let missionIdentifiers : Array; + + // it has any type in the DeployCampaign class + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let config : any; + + before(async () => { + hardhatMock = new HardhatMock(); + + config = { + env: "prod", + deployAdmin: { + address: "0xdeployAdminAddress", + }, + postDeploy: { + tenderlyProjectSlug: "tenderlyProject", + monitorContracts: true, + verifyContracts: true, + }, + }; + + const signerMock = { + address: "0xsignerAddress", + }; + + const deployer = new HardhatDeployer({ + hre: hardhatMock, + signer: signerMock, + env: "prod", + }); + + const contractsVersion = "1.7.9"; + const dbVersion = "109381236293746234"; + + const mongoAdapterMock = new MongoDBAdapter({ + logger: loggerMock, + dbUri: "mongodb://mockedMongo", + dbName: "TestDatabase", + mongoClientClass: MongoClientMock, + versionerClass: DBVersioner, + dbVersion, + contractsVersion, + }); + + await mongoAdapterMock.initialize(dbVersion); + + missionIdentifiers = [ + "buildObject", + "needsDeploy", + "deployed", + "proxyPost", + ]; + + const postDeployRun = [ + false, + false, + false, + ]; + + campaign = new DeployCampaign({ + logger: loggerMock, + missions: await testMissions( + missionIdentifiers, + postDeployRun + ), + deployer, + dbAdapter: mongoAdapterMock, + config, + }); + + await campaign.execute(); + }); + + describe("#deploy()", () => { + it("Should deploy all contracts from `missionIdentifiers`", async () => { + for (const mission of missionIdentifiers) { + assert.equal( + await campaign.state.contracts[mission].getAddress(), + `0xcontractAddress_Contract_${mission}` + ); + // does it make sense to do this? + assert.deepEqual( + await campaign.state.instances[mission].deployArgs(), + [`arg_${mission}1`, `arg_${mission}2`] + ); + } + }); + + it("#savetoDB() Should call `saveToDB()` when deploy a contract", async () => { + for (const mission of missionIdentifiers) { + if (mission !== "deployed") { + assert.equal( + // ts complains about `called` prop + // @ts-ignore + await campaign.state.instances[mission].called.includes("saveToDB"), + true + ); + } + } + }); + }); + + describe("Minor methods", () => { + it("Should update state of contracts in campaign, when deploys them", async () => { + const state = await campaign.state.contracts; + + // check here that all objects of contracts got into the state + assert.deepEqual( + Object.keys(state), + missionIdentifiers, + ); + + // double check that these objects look like the contracts + for (const mission of missionIdentifiers) { + const contract = state[mission]; + assertIsContract(contract); + } + }); + + it("#buildObject() Should build correct object of contract and call insertOne()", async () => { + const { + buildObject, + } = campaign.state.instances; + + const { + buildObject: contractBuildObject, + } = campaign.state.contracts; + + const buildedDbObject = await buildObject.buildDbObject(contractBuildObject, buildObject.implAddress); + + const resultBuildedDbObject = { + address: "0xcontractAddress_Contract_buildObject", + abi: "[]", + bytecode: "0xbytecode", + implementation: null, + name: "Contract_buildObject", + }; + + assert.deepEqual( + buildedDbObject, + resultBuildedDbObject + ); + }); + }); + + describe("#needsDeploy()",() => { + it("Should return TRUE because the contract is NOT in the DB", async () => { + assert.equal( + await campaign.state.instances.needsDeploy.needsDeploy(), + true + ); + }); + + it("Should return FALSE because the contract exists in the DB", async () => { + assert.equal( + await campaign.state.instances.deployed.needsDeploy(), + false + ); + }); + + it("Should write the contract into the state from the DB " + + "when #needsDeploy() returns FALSE", async () => { + const contractFromDB = await campaign.dbAdapter.getContract( + "Contract_deployed" + ); + + assert.equal( + await campaign.state.instances.deployed.needsDeploy(), + false + ); + + assert.equal( + await campaign.state.contracts.deployed.getAddress(), + contractFromDB?.address + ); + }); + }); +}); \ No newline at end of file diff --git a/test/cases/DBVersioner.test.ts b/test/cases/DBVersioner.test.ts new file mode 100644 index 0000000..1c399f3 --- /dev/null +++ b/test/cases/DBVersioner.test.ts @@ -0,0 +1,507 @@ +import assert from "node:assert"; +import { + COLL_NAMES, + DBVersioner, + VERSION_TYPES, +} from "../../src"; +import { loggerMock } from "../mocks/logger"; +import { dbMock } from "../mocks/mongo"; + + +describe("DB versioner", () => { + let versioner : DBVersioner; + + // Error for Date.now(). Leave +/- 2 seconds for code execution + const allowedTimeDifference = 2; + + const contractsVersion = "1.7.9"; + const dbVersion = "109381236293746234"; + + const tempVersion = "123[temp]"; + const deployedVersion = "456[deployed]"; + + // expected order of DB method calls when calling finalizeDeployedVersion() + const expectedOrder = [ + "updateOne", + "insertOne", + "deleteOne", + "updateOne", + ]; + + // order of DB methods called by dbVersioner + let called : Array<{ + method : string; + args : any; + }> = []; + + beforeEach(async () => { + versioner = new DBVersioner({ + dbVersion, + contractsVersion, + archive: false, + logger: loggerMock, + }); + versioner.versions = dbMock.collection(COLL_NAMES.versions); + + // override the mock methods to track the execution + // @ts-ignore + dbMock.collection().insertOne = async doc => { + called.push({ + method: "insertOne", + args: doc, + }); + return Promise.resolve(doc); + }; + + // @ts-ignore + dbMock.collection().updateOne = async (filter, update) => { + called.push({ + method: "updateOne", + args: { + filter, + update, + }, + }); + return Promise.resolve(); + }; + + // @ts-ignore + dbMock.collection().deleteOne = async filter => { + called.push({ + method: "deleteOne", + args: filter, + }); + return Promise.resolve(); + }; + + // @ts-ignore + dbMock.collection().deleteMany = async filter => { + called.push({ + method: "deleteMany", + args: filter, + }); + return Promise.resolve(); + }; + }); + + afterEach(() => { + called = []; + }); + + describe("TEMP and DEPLOYED versions do NOT exist", async () => { + beforeEach(async () => { + // @ts-ignore. Because native .collection() requires arguments + dbMock.collection().findOne = async args => { + if (args.type === "TEMP" || args.type === "DEPLOYED") return null; + }; + }); + + it("Should make a DB version (Date.now()) when it does NOT exist", async () => { + // do Math.abs, so that the error can be in both directions + assert.ok( + Math.abs( + Number( + await versioner.configureVersioning(dbMock) + ) - + Number(Date.now()) + ) <= allowedTimeDifference, + ); + }); + + it("Should make NEW final TEMP version (Date.now())", async () => { + assert.ok( + // do Math.abs, so that the error can be in both directions + Math.abs( + Number(await versioner.configureVersioning(dbMock)) - + Number(Date.now()) + ) <= allowedTimeDifference, + ); + }); + + // TODO: What to do with it? + it.skip("Should maintain the order of method calls (`expectedOrder`) when calls #finalizeDeloyedVersion()" + + "WITHOUT passed version", async () => { + await versioner.finalizeDeployedVersion(); + + // looking at the methods called by db, check the order of them and their arguments + called.forEach((calledMethod, index) => { + const methodName = calledMethod.method; + const args = calledMethod.args; + + assert.equal( + methodName, + expectedOrder[index] + ); + + if (methodName === "deleteOne") { + assert.equal( + args.type, + VERSION_TYPES.temp + ); + assert.equal( + args.dbVersion, + tempVersion + ); + } + + if (methodName === "insertOne") { + assert.equal( + args.type, + VERSION_TYPES.deployed + ); + assert.equal( + args.dbVersion, + tempVersion + ); + assert.equal( + args.contractsVersion, + contractsVersion + ); + } + + if (methodName === "updateOne") { + assert.equal( + args.filter.type, + index === 0 ? VERSION_TYPES.deployed : VERSION_TYPES.temp + ); + assert.equal( + args.update.$set.type, + VERSION_TYPES.archived + ); + } + }); + }); + }); + + describe("TEMP version does NOT exist", () => { + beforeEach(async () => { + // @ts-ignore + dbMock.collection().findOne = async ( + args : { + type : string; + } + ) => { + if (args.type === VERSION_TYPES.deployed) { + return { + dbVersion: deployedVersion, + type: VERSION_TYPES.deployed, + }; + } + + called.push({ + method: "findOne", + args, + }); + + return null; + }; + }); + + it("Should return NEW final TEMP version (Date.now())", async () => { + assert.ok( + // do Math.abs, so that the error can be in both directions + Math.abs( + Number( + await versioner.configureVersioning(dbMock) + ) - + Number(Date.now()) + ) <= allowedTimeDifference, + ); + }); + }); + + describe("DEPLOYED version does NOT exist", () => { + beforeEach(async () => { + // @ts-ignore + dbMock.collection().findOne = async ( + args : { + type : string; + } + ) => { + if (args.type === VERSION_TYPES.temp) { + return { + dbVersion: tempVersion, + type: VERSION_TYPES.temp, + }; + } + + return null; + }; + }); + + // TODO: make this + it.skip("if (!deployedV || version !== deployedV.dbVersion) #configureVersioning()", async () => { + assert.equal( + await versioner.configureVersioning(dbMock), + tempVersion + ); + }); + + it("Should return TEMP version when call #configureVersioning()", async () => { + assert.equal( + await versioner.configureVersioning(dbMock), + tempVersion + ); + }); + + it("Should maintain the order of method calls (`expectedOrder`) when calls #finalizeDeloyedVersion()" + + "WITHOUT passed version", async () => { + await versioner.finalizeDeployedVersion(); + + // looking at the methods called by db, check the order of them and their arguments + called.forEach((calledMethod, index) => { + const methodName = calledMethod.method; + const args = calledMethod.args; + + assert.equal( + methodName, + expectedOrder[index] + ); + + if (methodName === "deleteOne") { + assert.equal( + args.type, + VERSION_TYPES.temp + ); + assert.equal( + args.dbVersion, + tempVersion + ); + } + + if (methodName === "insertOne") { + assert.equal( + args.type, + VERSION_TYPES.deployed + ); + assert.equal( + args.dbVersion, + tempVersion + ); + assert.equal( + args.contractsVersion, + contractsVersion + ); + } + + if (methodName === "updateOne") { + assert.equal( + args.filter.type, + index === 0 ? VERSION_TYPES.deployed : VERSION_TYPES.temp + ); + assert.equal( + args.update.$set.type, + VERSION_TYPES.archived + ); + } + }); + }); + }); + + describe("Has SIMILAR `temp` and `deployed` versions", () => { + const similarVersion = "similarVersion"; + + beforeEach(async () => { + // @ts-ignore + dbMock.collection().findOne = async ( + args : { + type : string; + } + ) => { + if (args.type === VERSION_TYPES.temp) { + return { + dbVersion: similarVersion, + type: VERSION_TYPES.temp, + }; + } + + if (args.type === VERSION_TYPES.deployed) { + return { + dbVersion: similarVersion, + type: VERSION_TYPES.deployed, + }; + } + + return null; + }; + }); + + it("Should call only #updateOne with correct args when run #finalizeDeloyedVersion()", async () => { + await versioner.finalizeDeployedVersion(); + + const methodName = called[0].method; + const args = called[0].args; + + assert.equal( + methodName, + "updateOne" + ); + assert.equal( + args.filter.type, + VERSION_TYPES.temp + ); + assert.equal( + args.update.$set.type, + VERSION_TYPES.archived + ); + }); + + // TODO with passed version + it.skip("Should #finalizeDeloyedVersion()", async () => { + await versioner.finalizeDeployedVersion(); + }); + }); + + describe("Has DIFFERENT `temp` and `deployed` versions", () => { + beforeEach(async () => { + // @ts-ignore + dbMock.collection().findOne = async ( + args : { + type : string; + } + ) => { + if (args.type === VERSION_TYPES.temp) { + return { + dbVersion: tempVersion, + type: VERSION_TYPES.temp, + }; + } + + if (args.type === VERSION_TYPES.deployed) { + return { + dbVersion: deployedVersion, + type: VERSION_TYPES.deployed, + }; + } + + return null; + }; + }); + + it("Should return existing temp version when both, deployed and temp, exist", async () => { + assert.equal( + await versioner.configureVersioning(dbMock), + tempVersion + ); + }); + + it("Should maintain the order of method calls (`expectedOrder`) when calls #finalizeDeloyedVersion()" + + "WITHOUT passed version", async () => { + await versioner.finalizeDeployedVersion(); + + // looking at the methods called by db, check the order of them and their arguments + called.forEach((calledMethod, index) => { + const methodName = calledMethod.method; + const args = calledMethod.args; + + assert.equal( + methodName, + expectedOrder[index] + ); + + if (methodName === "deleteOne") { + assert.equal( + args.type, + VERSION_TYPES.temp + ); + assert.equal( + args.dbVersion, + tempVersion + ); + } + + if (methodName === "insertOne") { + assert.equal( + args.type, + VERSION_TYPES.deployed + ); + assert.equal( + args.dbVersion, + tempVersion + ); + assert.equal( + args.contractsVersion, + contractsVersion + ); + } + + if (methodName === "updateOne") { + assert.equal( + args.filter.type, + index === 0 ? VERSION_TYPES.deployed : VERSION_TYPES.temp + ); + assert.equal( + args.update.$set.type, + VERSION_TYPES.archived + ); + } + }); + }); + }); + + + describe("Minor methods", () => { + it("#createUpdateTempVersion() should call #updateOne() method with correct args", async () => { + await versioner.createUpdateTempVersion(tempVersion); + + const args = called[0].args; + + assert.equal( + called[0].method, + "updateOne" + ); + assert.equal( + args.filter.type, + VERSION_TYPES.temp + ); + assert.equal( + args.update.$set.dbVersion, + tempVersion + ); + assert.equal( + args.update.$set.contractsVersion, + contractsVersion + ); + }); + + it("#clearDBForVersion() should call #deleteMany() (WITHOUT passed `db`)", async () => { + await versioner.clearDBForVersion(tempVersion); + + const args = called[0].args; + + assert.equal( + called[0].method, + "deleteMany" + ); + assert.equal( + args.dbVersion, + tempVersion + ); + }); + + it("#clearDBForVersion() should call deleteMany() TWO times (WITH passed `db`)", async () => { + await versioner.clearDBForVersion(tempVersion, dbMock); + + called.forEach(calledMethod => { + const args = calledMethod.args; + + assert.equal( + calledMethod.method, + "deleteMany" + ); + + if (args.hasOwnProperty("version")) { + assert.equal( + args.version, + tempVersion + ); + } else { + assert.equal( + args.dbVersion, + tempVersion + ); + } + }); + }); + }); +}); diff --git a/test/cases/DeployCampaignSmoke.test.ts b/test/cases/DeployCampaignSmoke.test.ts index 07288f9..d291856 100644 --- a/test/cases/DeployCampaignSmoke.test.ts +++ b/test/cases/DeployCampaignSmoke.test.ts @@ -1,16 +1,24 @@ // eslint-disable-next-line max-len /* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, @typescript-eslint/ban-ts-comment, @typescript-eslint/no-explicit-any */ -import { Db, DbOptions, MongoClient, MongoClientOptions } from "mongodb"; import assert from "assert"; import { - DBVersioner, DeployCampaign, + DBVersioner, + DeployCampaign, HardhatDeployer, - IContractState, IDeployCampaignConfig, - IHardhatBase, ISignerBase, MongoDBAdapter, - TLogger, + IContractState, + IDeployCampaignConfig, + IHardhatBase, + ISignerBase, + MongoDBAdapter, } from "../../src"; -import { ATestDeployMission, makeMissionMock } from "../mocks/missions"; +import { + ATestDeployMission, + testMissions, +} from "../mocks/missions"; import { HardhatMock } from "../mocks/hardhat"; +import { MongoClientMock } from "../mocks/mongo"; +import { loggerMessages, loggerMock } from "../mocks/logger"; +import { signerMock } from "../mocks/accounts"; describe("Deploy Campaign Smoke Test", () => { @@ -18,7 +26,6 @@ describe("Deploy Campaign Smoke Test", () => { let missionIdentifiers : Array; let hardhatMock : HardhatMock; let config : any; - let loggerMessages : Array; before(async () => { config = { @@ -34,76 +41,14 @@ describe("Deploy Campaign Smoke Test", () => { }, }; - loggerMessages = []; - - const loggerMock = { - info: (msg : string) => { - loggerMessages.push(msg); - }, - error: () => {}, - debug: () => {}, - log: () => {}, - } as unknown as TLogger; - hardhatMock = new HardhatMock(); - const providerMock = { - waitForTransaction: async ( - txHash : string, - confirmations ?: number | undefined, - timeout ?: number | undefined - ) => Promise.resolve({ - contractAddress: "0xcontractAddress", - }), - }; - - const signerMock = { - getAddress: async () => Promise.resolve("0xsignerAddress"), - address: "0xsignerAddress", - }; - const deployer = new HardhatDeployer({ hre: hardhatMock, signer: signerMock, env: "prod", }); - const collectionMock = { - insertOne: async () => Promise.resolve(), - findOne: async (args : any) => { - if (args.type) { - return { - dbVersion: "109381236293746234", - }; - } - }, - updateOne: async () => Promise.resolve(), - deleteMany: async () => Promise.resolve(), - deleteOne: async () => Promise.resolve(), - }; - - const dbMock = { - collection: () => (collectionMock), - }; - - class MongoClientMock extends MongoClient { - constructor (dbUri : string, clientOpts : MongoClientOptions) { - super(dbUri, clientOpts); - } - - async connect () { - return Promise.resolve(this); - } - - db (dbName ?: string | undefined, options ?: DbOptions | undefined) { - return dbMock as unknown as Db; - } - - async close (force ?: boolean) { - await Promise.resolve(); - } - } - const contractsVersion = "1.7.9"; const dbVersion = "109381236293746234"; @@ -131,24 +76,12 @@ describe("Deploy Campaign Smoke Test", () => { false, ]; - const testMissions = missionIdentifiers.map( - (id, idx) => makeMissionMock({ - _contractName: `Contract${id}`, - _instanceName: `${id}`, - _deployArgs: [ - `arg${id}1`, - `arg${id}2`, - ], - _isProxy: id.includes("proxy"), - _needsPostDeploy: id === missionIdentifiers[2], - _postDeployCb: async () => { - postDeployRun[idx] = true; - }, - })); - campaign = new DeployCampaign({ logger: loggerMock, - missions: testMissions, + missions: testMissions( + missionIdentifiers, + postDeployRun + ), deployer, dbAdapter: mongoAdapterMock, config, @@ -167,7 +100,7 @@ describe("Deploy Campaign Smoke Test", () => { assert.equal(Object.keys(instances).length, missionIdentifiers.length); missionIdentifiers.forEach(id => { - assert.equal(instances[id].contractName, `Contract${id}`); + assert.equal(instances[id].contractName, `Contract_${id}`); assert.equal(instances[id].instanceName, id); assert.equal(instances[id].proxyData.isProxy, id.includes("proxy")); }); diff --git a/test/cases/GetAdapter.test.ts b/test/cases/GetAdapter.test.ts new file mode 100644 index 0000000..3da59ed --- /dev/null +++ b/test/cases/GetAdapter.test.ts @@ -0,0 +1,146 @@ +import assert from "node:assert"; +import { + DEFAULT_MONGO_DB_NAME, + DEFAULT_MONGO_URI, + getLogger, + getMongoAdapter, + MongoDBAdapter, + resetMongoAdapter, +} from "../../src"; +import { loggerMock } from "../mocks/logger"; + + +describe("Get Adapter", () => { + let originalInitialize : any; + let mongoAdapter : MongoDBAdapter; + + process.env.SILENT_LOGGER = "true"; + + const mockLogger = loggerMock; + + beforeEach(() => { + resetMongoAdapter(); + + originalInitialize = MongoDBAdapter.prototype.initialize; + + // @ts-ignore + MongoDBAdapter.prototype.initialize = async function () { + return Promise.resolve(); + }; + }); + + afterEach(() => { + MongoDBAdapter.prototype.initialize = originalInitialize; + }); + + it("Should create a new MongoDBAdapter instance if no existing adapter", async () => { + mongoAdapter = await getMongoAdapter(); + assert(mongoAdapter instanceof MongoDBAdapter); + }); + + it.skip("Should reset mongoAdapter to NULL after resetMongoAdapter() is called", async () => { + resetMongoAdapter(); + assert.strictEqual( + mongoAdapter, + null + ); + }); + + it("Should use the default logger if it's not passed", async () => { + const defaultLogger = getLogger(); + const mongoAdapter = await getMongoAdapter(); + assert.deepEqual( + mongoAdapter.logger, + defaultLogger + ); + }); + + it("Should use the provided logger if passed", async () => { + const mongoAdapter = await getMongoAdapter({ logger: mockLogger }); + assert.strictEqual( + mongoAdapter.logger, + mockLogger + ); + }); + + it("Should create a new adapter if dbUri or dbName changes", async () => { + const mongoAdapter1 = await getMongoAdapter(); + process.env.MONGO_DB_URI = "mongodb://address"; + const mongoAdapter2 = await getMongoAdapter(); + assert.notStrictEqual( + mongoAdapter1, + mongoAdapter2 + ); + }); + + it("Should pass the contractsVersion to the MongoDBAdapter", async () => { + const contractsVersion = "1.0.0"; + const mongoAdapter = await getMongoAdapter({ + logger: mockLogger, + contractsVersion, + }); + assert.equal( + mongoAdapter.versioner.contractsVersion, + contractsVersion + ); + }); + + it("Should handle missing environment variables and use defaults", async () => { + delete process.env.MONGO_DB_URI; + delete process.env.MONGO_DB_NAME; + const mongoAdapter = await getMongoAdapter(); + assert.strictEqual( + mongoAdapter.dbUri, + DEFAULT_MONGO_URI + ); + assert.strictEqual( + mongoAdapter.dbName, + DEFAULT_MONGO_DB_NAME + ); + }); + + it("Should set version to undefined if environment variable is missing", async () => { + delete process.env.MONGO_DB_VERSION; + const mongoAdapter = await getMongoAdapter(); + + // It stays under todo. (Returns "0" if nothing passed) + assert.strictEqual( + mongoAdapter.versioner.curDbVersion, + "0" + ); + }); + + it("Should set archive flag to true if ARCHIVE_PREVIOUS_DB_VERSION is TRUE", async () => { + process.env.ARCHIVE_PREVIOUS_DB_VERSION = "true"; + const mongoAdapter = await getMongoAdapter(); + assert.strictEqual( + mongoAdapter.versioner.archiveCurrentDeployed, + true + ); + }); + + it("Should set archive flag to FALSE if ARCHIVE_PREVIOUS_DB_VERSION is not true", async () => { + process.env.ARCHIVE_PREVIOUS_DB_VERSION = "false"; + const mongoAdapter = await getMongoAdapter(); + assert.strictEqual( + mongoAdapter.versioner.archiveCurrentDeployed, + false + ); + }); + + it("Should handle absence of MONGO_DB_URI and MONGO_DB_NAME", async () => { + delete process.env.MONGO_DB_URI; + delete process.env.MONGO_DB_NAME; + + const mongoAdapter = await getMongoAdapter(); + + assert.strictEqual( + mongoAdapter.dbUri, + DEFAULT_MONGO_URI + ); + assert.strictEqual( + mongoAdapter.dbName, + DEFAULT_MONGO_DB_NAME + ); + }); +}); diff --git a/test/cases/HardhatDeployerUnit.test.ts b/test/cases/HardhatDeployerUnit.test.ts new file mode 100644 index 0000000..72347b2 --- /dev/null +++ b/test/cases/HardhatDeployerUnit.test.ts @@ -0,0 +1,134 @@ +import assert from "node:assert"; +import { HardhatDeployer } from "../../src"; +import { signerMock } from "../mocks/accounts"; +import { HardhatMock } from "../mocks/hardhat"; +import { assertIsContract } from "../helpers/isContractCheck"; + + +describe("Hardhat Deployer", () => { + let hardhatMock : HardhatMock; + let hhDeployer : any; + + beforeEach(async () => { + hardhatMock = new HardhatMock(); + hhDeployer = new HardhatDeployer({ + hre: hardhatMock, + signer: signerMock, + env: "prod", + }); + }); + + it("#getFactory() Should call getContractFactory with correct params", async () => { + const contractName = "contractFactory"; + + await hhDeployer.getFactory(contractName); + + const callStack = hardhatMock.called[0]; + + assert.equal( + callStack.methodName, + "getContractFactory" + ); + assert.equal( + callStack.args.contractName, + contractName + ); + assert.equal( + callStack.args.signerOrOptions.address, + signerMock.address + ); + }); + + it("#getContractObject() Should return contract object with attached address", async () => { + const contractName = "contractObject"; + const contractAddress = `0xcontractAddress_${contractName}`; + + const contractObject = await hhDeployer.getContractObject( + contractName, + signerMock.address + ); + + const callStack = hardhatMock.called; + + assert.equal( + callStack[1].methodName, + "attach" + ); + assert.equal( + contractObject.target, + contractAddress + ); + assert.equal( + await contractObject.getAddress(), + contractAddress + ); + assert.equal( + await contractObject.getAddress(), + contractAddress + ); + + assert.equal( + callStack[0].methodName, + "getContractFactory" + ); + // make sure it's contract + assertIsContract(contractObject); + }); + + it("#deployProxy() Should call deployProxy with correct arguments and return the contract", async () => { + const contractName = "deployProxy"; + const contractArgs = { + contractName, + args: [`arg_${contractName}_1`, `arg_${contractName}_2`], + kind: "uups", + }; + + const contract = await hhDeployer.deployProxy(contractArgs); + + const callStack = hardhatMock.called; + + assert.equal( + callStack[1].methodName, + "deployProxy" + ); + assert.deepEqual( + callStack[1].args, + contractArgs + ); + + // extra checks + assert.equal( + callStack[0].methodName, + "getContractFactory" + ); + assertIsContract(contract); + }); + + it("#deployContract() Should call deploy with correct arguments and return the contract", async () => { + const contractName = "deployContract"; + const contractArgs = [`arg_${contractName}_1`, `arg_${contractName}_2`]; + + const contract = await hhDeployer.deployContract( + contractName, + contractArgs + ); + + const callStack = hardhatMock.called; + + assert.equal( + callStack[1].methodName, + "deploy" + ); + assert.deepEqual( + callStack[1].args, + contractArgs + ); + + // extra checks + assert.equal( + callStack[0].methodName, + "getContractFactory" + ); + assertIsContract(contract); + }); +}); \ No newline at end of file diff --git a/test/helpers/isContractCheck.ts b/test/helpers/isContractCheck.ts new file mode 100644 index 0000000..180b989 --- /dev/null +++ b/test/helpers/isContractCheck.ts @@ -0,0 +1,33 @@ +import assert from "node:assert"; + +export const assertIsContract = (contract : any) => { + assert.strictEqual( + typeof contract.deploymentTransaction, + "function", + "Not a contract. Method 'deploymentTransaction' should exist" + ); + + assert.strictEqual( + typeof contract.getAddress, + "function", + "Not a contract. Method 'getAddress' should exist" + ); + + assert.strictEqual( + typeof contract.interface, + "object", + "Not a contract. Property 'interface' should exist" + ); + + assert.strictEqual( + typeof contract.target, + "string", + "Not a contract. Property 'target' should exist and be an address" + ); + + assert.strictEqual( + typeof contract.waitForDeployment, + "function", + "Not a contract. Function 'waitForDeployment' should exist" + ); +}; diff --git a/test/mocks/accounts.ts b/test/mocks/accounts.ts new file mode 100644 index 0000000..7e5c873 --- /dev/null +++ b/test/mocks/accounts.ts @@ -0,0 +1,13 @@ +export const signerMock = { + address: "0xsignerAddress", +}; + +export const providerMock = { + waitForTransaction: async ( + txHash : string, + confirmations ?: number | undefined, + timeout ?: number | undefined + ) => Promise.resolve({ + contractAddress: "0xcontractAddress", + }), +}; \ No newline at end of file diff --git a/test/mocks/hardhat.ts b/test/mocks/hardhat.ts index 91ec5f5..8160155 100644 --- a/test/mocks/hardhat.ts +++ b/test/mocks/hardhat.ts @@ -3,28 +3,44 @@ import { IContractArtifact, IContractFactoryBase, IContractV6, - IHardhatBase, IHHSubtaskArguments, ISignerBase, + IHardhatBase, + IHHSubtaskArguments, TDeployArgs, THHTaskArguments, TProxyKind, } from "../../src"; -const contractMock = { - target: "0xcontractAddress", - getAddress: async () => Promise.resolve("0xcontractAddress"), - waitForDeployment: async () => Promise.resolve(contractMock), +const contractMock = (name : string) => ({ + target: `0xcontractAddress_${name}`, + getAddress: async () => Promise.resolve(`0xcontractAddress_${name}`), + waitForDeployment: async () => Promise.resolve(contractMock(name)), deploymentTransaction: () => ({ - hash: "0xhash", + hash: `0xhash_${name}`, }), interface: {}, -} as IContractV6; +} as unknown as IContractV6); -export const contractFactoryMock = { - deploy: async () => Promise.resolve(contractMock), - attach: async () => Promise.resolve(contractMock), - contractName: "", -} as unknown as IContractFactoryBase; +export const contractFactoryMock = (name : string, called : Array) => ({ + // @ts-ignore // doesn't see, that TDeployArgs type reflects an array + deploy: async (...args ?: TDeployArgs) => { + called.push({ + methodName: "deploy", + args, + }); + + return Promise.resolve(contractMock(name)); + }, + attach: async () => { + called.push({ + methodName: "attach", + args: [name], + }); + + return Promise.resolve(contractMock(name)); + }, + contractName: name, +} as unknown as IContractFactoryBase); export interface IExecutedCall { methodName : string; @@ -36,14 +52,23 @@ export class HardhatMock implements IHardhatBase { called : Array = []; ethers = { - getContractFactory: async - // eslint-disable-next-line @typescript-eslint/no-unused-vars - (contractName : string, signerOrOptions : any) : Promise => ( - { - ...contractFactoryMock, + getContractFactory: async ( + contractName : string, + signerOrOptions : any + ) : Promise => { + + const factory = { + ...contractFactoryMock(contractName, this.called), contractName, - } - ) as unknown as Promise, + } as unknown as F; + + this.called.push({ + methodName: "getContractFactory", + args: { contractName, signerOrOptions }, + }); + + return Promise.resolve(factory); + }, provider: { getCode: async () => Promise.resolve("0xbytecode"), }, @@ -53,14 +78,20 @@ export class HardhatMock implements IHardhatBase { deployProxy: async ( factory : any, args : TDeployArgs, - options : { kind : TProxyKind; } + options : { + kind : TProxyKind; + } ) : Promise => { this.called.push({ methodName: "deployProxy", - args: { contractName: factory.contractName, args, kind: options.kind }, + args: { + contractName: factory.contractName, + args, + kind: options.kind, + }, }); - return contractMock as unknown as Promise; + return contractMock(factory.contractName) as unknown as Promise; }, erc1967: { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/test/mocks/logger.ts b/test/mocks/logger.ts new file mode 100644 index 0000000..d8cdc05 --- /dev/null +++ b/test/mocks/logger.ts @@ -0,0 +1,13 @@ +import { TLogger } from "../../src"; + +export let loggerMessages : Array; +loggerMessages = []; + +export const loggerMock = { + info: (msg : string) => { + loggerMessages.push(msg); + }, + error: () => {}, + debug: () => {}, + log: () => {}, +} as unknown as TLogger; \ No newline at end of file diff --git a/test/mocks/missions.ts b/test/mocks/missions.ts index d9190a9..302c9d9 100644 --- a/test/mocks/missions.ts +++ b/test/mocks/missions.ts @@ -3,13 +3,27 @@ import { BaseDeployMission, IContractState, IDeployCampaignConfig, IDeployMissionArgs, IHardhatBase, - IProviderBase, ISignerBase, ProxyKinds, TDeployArgs, } from "../../src"; import { IExecutedCall } from "./hardhat"; +export const testMissions = ( + missionIdentifiers : Array, + postDeployRun : Array +) => missionIdentifiers.map((id, idx) => + makeMissionMock({ + _contractName: `Contract_${id}`, + _instanceName: `${id}`, + _deployArgs: [`arg_${id}1`, `arg_${id}2`], + _isProxy: id.includes("proxy"), + _needsPostDeploy: id === missionIdentifiers[2], + _postDeployCb: async () => { + postDeployRun[idx] = true; + }, + }) +); export const makeTestMissionProxy = (mission : any) => new Proxy(mission, { get: (target, prop) => { diff --git a/test/mocks/mongo.ts b/test/mocks/mongo.ts new file mode 100644 index 0000000..aa2d076 --- /dev/null +++ b/test/mocks/mongo.ts @@ -0,0 +1,77 @@ +import { + MongoClient, + MongoClientOptions, + Db, + DbOptions, + WriteConcern, + ReadConcern, +} from "mongodb"; + +export const collectionMock = { + insertOne: async () => Promise.resolve(), + findOne: async ( + args : { + name : string; + version : string; + type : string; + } + ) => { + if (args.name === "Contract_deployed") { + return { + name: `${args.name}`, + address: `0xcontractAddress_${args.name}`, + abi: "[]", + bytecode: "0xbytecode", + implementation: null, + version: "109355555555555555", + }; + } + + if ( + args.type === "TEMP" || + args.type === "DEPLOYED" || + args.type === "ARCHIVED" + ) { + return { + dbVersion: "109381236293746234", + }; + } + }, + updateOne: async () => Promise.resolve(), + deleteMany: async () => Promise.resolve(), + deleteOne: async () => Promise.resolve(), +}; + +export const dbMock = { + collection: () => collectionMock, + databaseName: "mockDb", + options: {}, + readConcern: new ReadConcern("local"), + writeConcern: new WriteConcern(), + secondaryOk: true, + readPreference: { mode: "primary" }, + command: async () => Promise.resolve({}), + aggregate: () => ({ + toArray: async () => Promise.resolve([]), + }), +} as unknown as Db; + +export class MongoClientMock extends MongoClient { + constructor (dbUri : string, clientOpts : MongoClientOptions) { + super(dbUri, clientOpts); + } + + async connect () { + return Promise.resolve(this); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + db (dbName ?: string | undefined, options ?: DbOptions | undefined) { + return dbMock as unknown as Db; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async close (force ?: boolean) { + await Promise.resolve(); + } +} diff --git a/tsconfig.json b/tsconfig.json index 42d0007..3ea02ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ }, "compilerOptions": { "outDir": "./dist", - "rootDir": "./src", + "rootDir": "./", "target": "es2022", "module": "node16", "moduleResolution": "node16", @@ -19,8 +19,11 @@ "noImplicitAny": true, "declaration": true }, + "include": [ + "src/**/*", + "test/**/*.ts" + ], "exclude": [ - "./test/**/*", "./dist/**/*" ] } \ No newline at end of file