diff --git a/ZelBack/config/default.js b/ZelBack/config/default.js index c14075bd5a..63baaa3751 100644 --- a/ZelBack/config/default.js +++ b/ZelBack/config/default.js @@ -83,6 +83,12 @@ module.exports = { fluxTeamFluxID: '1hjy4bCYBJr4mny4zCE85J94RXa8W6q37', fluxSupportTeamFluxID: '16iJqiVbHptCx87q6XQwNpKdgEZnFtKcyP', deterministicNodesStart: 558000, + preProd: { + probability: 0.07, + remote: 'https://github.com/RunOnFlux/flux.git', + branch: 'preprod', + daysToNextEval: 30, + }, fluxapps: { // in flux main chain per month (blocksLasting) price: [ diff --git a/ZelBack/src/services/dbHelper.js b/ZelBack/src/services/dbHelper.js index 643147f44d..f5c3dd03f0 100644 --- a/ZelBack/src/services/dbHelper.js +++ b/ZelBack/src/services/dbHelper.js @@ -2,6 +2,10 @@ * @module Helper module used for all interactions with database */ +/** + * @import { MongoClient } from "mongodb" + */ + const mongodb = require('mongodb'); const config = require('config'); @@ -23,7 +27,7 @@ function databaseConnection() { * * @param {string} [url] * - * @returns {object} mongodb.MongoClient + * @returns {MongoClient} */ async function connectMongoDb(url) { const connectUrl = url || mongoUrl; @@ -32,21 +36,22 @@ async function connectMongoDb(url) { useUnifiedTopology: true, maxPoolSize: 100, }; - const db = await MongoClient.connect(connectUrl, mongoSettings); - return db; + const client = await MongoClient.connect(connectUrl, mongoSettings); + return client; } /** * Initiates default db connection. - * @returns true + * @returns {MongoClient} */ async function initiateDB() { if (!openDBConnection) openDBConnection = await connectMongoDb(); - return true; + return openDBConnection; } /** * Closes DB connection if exists. + * @returns {Promise} */ async function closeDbConnection() { if (openDBConnection) { @@ -63,7 +68,7 @@ async function closeDbConnection() { * @param {string} distinct - field name * @param {object} [query] * - * @returns array + * @returns {Proimise} */ async function distinctDatabase(database, collection, distinct, query) { const results = await database.collection(collection).distinct(distinct, query); @@ -78,7 +83,7 @@ async function distinctDatabase(database, collection, distinct, query) { * @param {object} query * @param {object} [projection] * - * @returns array + * @returns {Promise} */ async function findInDatabase(database, collection, query, projection) { const results = await database.collection(collection).find(query, projection).toArray(); diff --git a/ZelBack/src/services/fluxService.js b/ZelBack/src/services/fluxService.js index 73680b467a..4bcadf7b61 100644 --- a/ZelBack/src/services/fluxService.js +++ b/ZelBack/src/services/fluxService.js @@ -24,6 +24,7 @@ const fluxNetworkHelper = require('./fluxNetworkHelper'); const geolocationService = require('./geolocationService'); const syncthingService = require('./syncthingService'); const dockerService = require('./dockerService'); +const fluxRepository = require('./utils/fluxRepository'); // for streamChain endpoint const zlib = require('node:zlib'); @@ -32,6 +33,8 @@ const tar = require('tar-fs'); // const stream = require('node:stream/promises'); const stream = require('node:stream'); +const fluxRepo = new fluxRepository.FluxRepository({ repoDir: process.cwd() }); + /** * Stream chain lock, so only one request at a time */ @@ -103,7 +106,6 @@ async function getCurrentCommitId(req, res) { * @returns {Promise} Message. */ async function getCurrentBranch(req, res) { - // ToDo: Fix - this breaks if head in detached state (or something similar) if (req) { const authorized = await verificationHelper.verifyPrivilege('adminandfluxteam', req); if (authorized !== true) { @@ -112,23 +114,24 @@ async function getCurrentBranch(req, res) { } } - const { stdout: commitId, error } = await serviceHelper.runCommand('git', { - logError: false, params: ['rev-parse', '--abbrev-ref', 'HEAD'], - }); - - if (error) { - const errMsg = messageHelper.createErrorMessage( - `Error getting current branch of Flux: ${error.message}`, - error.name, - error.code, - ); - return res ? res.json(errMsg) : errMsg; - } + // null branch is detached HEAD, or error + const branch = await fluxRepo.currentBranch(); - const successMsg = messageHelper.createSuccessMessage(commitId.trim()); + const successMsg = messageHelper.createSuccessMessage(branch); return res ? res.json(successMsg) : successMsg; } +/** + * If this node is on the preprod branch + * @returns {Promise} + */ +async function isPreProdNode() { + const currentBranch = await fluxRepo.currentBranch(); + const { preProd: { branch: preProdBranch } } = config; + + return currentBranch === preProdBranch; +} + /** * Check out branch if it exists locally * @param {string} branch The branch to checkout @@ -779,6 +782,7 @@ async function tailDaemonDebug(req, res) { } const defaultDir = daemonServiceUtils.getFluxdDir(); + const datadir = daemonServiceUtils.getConfigValue('datadir') || defaultDir; const filepath = path.join(datadir, 'debug.log'); @@ -1090,6 +1094,8 @@ async function getFluxInfo(req, res) { } info.flux.ip = ipRes.data; info.flux.staticIp = geolocationService.isStaticIP(); + const preProdNode = await isPreProdNode(); + info.flux.preProdNode = preProdNode; info.flux.maxNumberOfIpChanges = fluxNetworkHelper.getMaxNumberOfIpChanges(); const zelidRes = await getFluxZelID(); if (zelidRes.status === 'error') { @@ -1741,6 +1747,7 @@ module.exports = { getRouterIP, hardUpdateFlux, installFluxWatchTower, + isPreProdNode, isStaticIPapi, lockStreamLock, rebuildHome, diff --git a/ZelBack/src/services/utils/fluxRepository.js b/ZelBack/src/services/utils/fluxRepository.js new file mode 100644 index 0000000000..250af6c07e --- /dev/null +++ b/ZelBack/src/services/utils/fluxRepository.js @@ -0,0 +1,78 @@ +const path = require('node:path'); +const os = require('node:os'); +const sg = require('simple-git'); + +class FluxRepository { + // this may not exist + defaultRepoDir = path.join(os.homedir(), 'zelflux'); + + constructor(options = {}) { + this.repoPath = options.repoDir || this.defaultRepoDir; + + const gitOptions = { + baseDir: this.repoPath, + binary: 'git', + maxConcurrentProcesses: 6, + trimmed: true, + }; + + this.git = sg.simpleGit(gitOptions); + } + + async remotes() { + return this.git.getRemotes(true).catch(() => []); + } + + async addRemote(name, url) { + await this.git.addRemote(name, url).catch(() => { }); + } + + async currentCommitId() { + const id = await this.git.revparse('HEAD').catch(() => null); + return id; + } + + async currentBranch() { + const branches = await this.git.branch().catch(() => null); + if (!branches) return null; + + const { current, detached } = branches; + + return detached ? null : current; + } + + async resetToCommitId(id) { + await this.git.reset(sg.ResetMode.HARD, [id]).catch(() => { }); + } + + async switchBranch(branch, options = {}) { + const forceClean = options.forceClean || false; + const reset = options.reset || false; + const remote = options.remote || 'origin'; + const remoteBranch = `${remote}/${branch}`; + + // fetch first incase there are errors. + await this.git.fetch(remote, branch); + + if (forceClean) { + await this.git.clean(sg.CleanOptions.FORCE + sg.CleanOptions.RECURSIVE); + } + + if (reset) { + await this.git.reset(sg.ResetMode.HARD, [remoteBranch]); + } + + const exists = await this.git.revparse(['--verify', branch]).catch(() => false); + + if (exists) { + await this.git.checkout(branch); + // don't think we need to reset here + await this.git.merge(['--ff-only']); + return; + } + + await this.git.checkout(['--track', remoteBranch]); + } +} + +module.exports = { FluxRepository }; diff --git a/apiServer.js b/apiServer.js index bb9f267c7c..66c90db71a 100644 --- a/apiServer.js +++ b/apiServer.js @@ -1,4 +1,8 @@ -global.userconfig = require('./config/userconfig'); +/** + * @import { MongoClient, Collection } from "mongodb" + */ + +globalThis.userconfig = require('./config/userconfig'); if (typeof AbortController === 'undefined') { // polyfill for nodeJS 14.18.1 - without having to use experimental features @@ -23,10 +27,12 @@ const fluxServer = require('./ZelBack/src/lib/fluxServer'); const log = require('./ZelBack/src/lib/log'); +const dbHelper = require('./ZelBack/src/services/dbHelper'); const serviceManager = require('./ZelBack/src/services/serviceManager'); const serviceHelper = require('./ZelBack/src/services/serviceHelper'); const upnpService = require('./ZelBack/src/services/upnpService'); const requestHistoryStore = require('./ZelBack/src/services/utils/requestHistory'); +const fluxRepository = require('./ZelBack/src/services/utils/fluxRepository'); const apiPort = userconfig.initial.apiport || config.server.apiport; const apiPortHttps = +apiPort + 1; @@ -40,6 +46,148 @@ let axiosDefaultsSet = false; */ let cacheable = null; +/** + * A fairly random way of determining if a node is on the preprod branch or master + * @returns {boolean} + */ +function isPreProdNode() { + const chance = Math.random(); + return chance <= config.preProd.probability; +} + +/** + * Throws the dice to see if this node is a preprod node, and stores it in the + * local mongo state database + * @param {Collection} col + * @returns {Promise} + */ +async function setPreProdNode(col) { + const preprodNode = isPreProdNode(); + + await col.updateOne( + { key: 'isPreProd' }, + { $set: { key: 'isPreProd', value: preprodNode } }, + { upsert: true }, + ); + + return preprodNode; +} + +/** + * Checks the local mongo state database to see if this node has thrown the dice + * to see if it is a preprod node within the last daysToNextEval time period. + * @param {Collection} col + * @returns {Promise} + */ +async function getPreProdNode(col) { + const result = await col.findOne( + { key: 'isPreProd' }, + { projection: { value: 1 } }, + ); + + if (!result) return null; + + const { value: isPreprod, _id: id } = result; + + const timestamp = new Date(id.getTimestamp()); + + timestamp.setDate(timestamp.getDate() + config.preProd.daysToNextEval); + + if (timestamp < new Date()) return null; + + return isPreprod; +} + +/** + * Determines if this is a preprod node or production node. + * @param {MongoClient} client + * @returns {Promise} + */ +async function getPreProdState(client) { + const db = client.db('zelfluxlocal'); + const col = db.collection('state'); + col.createIndex({ key: 1 }, { unique: true }); + + const preprodNode = await getPreProdNode(col) ?? await setPreProdNode(col); + + return preprodNode; +} + +/** + * Chooses either preprod or production branches. Except if the node is on deveop, + * then nothing happens. If the branch is changed, fluxOS is restarted by Nodemon, + * if it is running. + * @param {MongoClient} client + * @param {string} repoDir + * @returns {Promise} + */ +async function setProductionBranch(client, repoDir) { + const { initial: { development, disablePreProd } } = userconfig; + // Develop nodes take priority over preProd nodes. + if (development || disablePreProd) return; + + const sleep = (ms) => new Promise((r) => { setTimeout(r, ms); }); + + const preprodNode = await getPreProdState(client); + + const logText = preprodNode ? 'pre-production' : 'production'; + log.info(`Fluxnode running in ${logText} mode`); + + const { preProd: { branch, remote } } = config; + + const targetBranch = preprodNode ? branch : 'master'; + + const repo = new fluxRepository.FluxRepository({ repoDir }); + const remotes = await repo.remotes(); + const currentBranch = await repo.currentBranch(); + + log.info(`Fluxnode on branch: ${currentBranch}`); + + const origin = remotes.find( + (r) => r.refs.fetch === remote, + ); + + // if we don't find the origin, something is fishy. Maybe git:// scheme, maybe a + // different origin. Either way, we let it go and continue on whatever branch is set. + if (!origin) { + log.warn(`Unable to find remote ref: ${remote} in remotes... skipping preprod setup`); + return; + } + + if (currentBranch === targetBranch) return; + + log.info(`Switching from branch: ${currentBranch} to: ${targetBranch}`); + + await repo.switchBranch(targetBranch, { + remote: origin.name, + forceClean: true, + reset: true, + }).catch((err) => log.info(err)); + + // nodemon should kill this process within 5 seconds as we've changed files. + + await sleep(10_000); + + // We're still here. Maybe no backend files changed with the branch switch. + // Lets trigger a restart. We're just updating the file access / modified + // times - which nodemon sees as files changed. + + const time = new Date(); + const testFile = path.join(repoDir, 'ZelBack/config/default.js'); + await fs.utimes(testFile, time, time).catch(() => { }); + + await sleep(10_000); + + // Without knowing for sure what the supervisor is, forking the current process + // just feels too risky. We just let it go, and continue running on our current branch. + + // We're still here. Doesn't seem like nodemon is running. Lets just fork + // ourselves then. + + // fork(process.argv[1], { detached: true }).unref(); + // process.exit(); +} + function getrequestHistory() { return requestHistory; } @@ -227,6 +375,12 @@ async function initiate() { process.exit(1); }); + const appRoot = process.cwd(); + + const dbClient = await dbHelper.initiateDB().catch(() => null); + + if (dbClient) await setProductionBranch(dbClient, appRoot); + await createDnsCache(); await loadUpnpIfRequired(); @@ -235,7 +389,6 @@ async function initiate() { configReload(); }, 2 * 1000); - const appRoot = process.cwd(); // ToDo: move this to async const certExists = fs.existsSync(path.join(appRoot, 'certs/v1.key')); diff --git a/package.json b/package.json index 8c31af38cc..6a2bc9e5a0 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "path": "~0.12.7", "path-to-regexp": "~6.2.2", "qs": "~6.11.2", + "simple-git": "~3.25.0", "socket.io": "~4.7.2", "splitargs": "~0.0.7", "store": "~2.0.12", diff --git a/tests/ZelBack/repoTests.js b/tests/ZelBack/repoTests.js new file mode 100644 index 0000000000..45f3d1353a --- /dev/null +++ b/tests/ZelBack/repoTests.js @@ -0,0 +1,151 @@ +const chai = require('chai'); +chai.use(require('chai-as-promised')); + +const { expect } = chai; + +const os = require('node:os'); +const fs = require('node:fs/promises'); +const path = require('node:path'); + +const { simpleGit } = require('simple-git'); + +const { FluxRepository } = require('../../ZelBack/src/services/utils/fluxRepository'); + +describe('Flux preprod branch tests', () => { + const repoName = 'flux-integration'; + const testRepoName = `${repoName}-test`; + + let testDir; + let repoDir; + let testRepoDir; + + before(async () => { + testDir = os.tmpdir(); + repoDir = path.join(testDir, repoName); + testRepoDir = path.join(testDir, testRepoName); + + await fs.mkdir(repoDir); + + console.log('Cloning test repository'); + const git = simpleGit(); + await git.clone('https://github.com/RunOnFlux/flux-integration.git', repoDir); + }); + + beforeEach(async () => { + await fs.cp(repoDir, testRepoDir, { recursive: true }); + }); + + afterEach(async () => { + await fs.rm(testRepoDir, { recursive: true, force: true }); + }); + + after(async () => { + console.log('Deleting test repository'); + await fs.rm(repoDir, { recursive: true, force: true }); + }); + + it('should return the correct branch as current branch', async () => { + const repo = new FluxRepository({ repoDir: testRepoDir }); + const branch = await repo.currentBranch(); + expect(branch).to.equal('master'); + }); + + it('should list remote branches', async () => { + const repo = new FluxRepository({ repoDir: testRepoDir }); + const expected = [ + { + name: 'origin', + refs: { + fetch: 'https://github.com/RunOnFlux/flux-integration.git', + push: 'https://github.com/RunOnFlux/flux-integration.git', + }, + }, + ]; + const remotes = await repo.remotes(); + expect(remotes).to.deep.equal(expected); + }); + + it('should switch branches if local is up to date with remote', async () => { + const repo = new FluxRepository({ repoDir: testRepoDir }); + await expect(repo.switchBranch('preprod')).to.be.fulfilled; + const branch = await repo.currentBranch(); + expect(branch).to.equal('preprod'); + }); + + it('should switch branches if local has untracked work', async () => { + const testFile = path.join(testRepoDir, 'untracked-file'); + await fs.writeFile(testFile, 'test content'); + + const repo = new FluxRepository({ repoDir: testRepoDir }); + await expect(repo.switchBranch('preprod')).to.be.fulfilled; + const branch = await repo.currentBranch(); + expect(branch).to.equal('preprod'); + + const fileExists = Boolean(await fs.stat(testFile).catch(() => false)); + expect(fileExists).to.equal(true); + }); + + it('should switch branches and remove untracked if local has untracked work', async () => { + const testFile = path.join(testRepoDir, 'untracked-file'); + await fs.writeFile(testFile, 'test content'); + + const repo = new FluxRepository({ repoDir: testRepoDir }); + await expect(repo.switchBranch('preprod', { forceClean: true })).to.be.fulfilled; + const branch = await repo.currentBranch(); + expect(branch).to.equal('preprod'); + + const fileExists = Boolean(await fs.stat(testFile).catch(() => false)); + expect(fileExists).to.equal(false); + }); + + it('should reset current branch and allow switch where local work would be overwritten', async () => { + const testFile = path.join(testRepoDir, 'README.md'); + await fs.writeFile(testFile, 'this file has been modified and would be overwritten on switch'); + + const repo = new FluxRepository({ repoDir: testRepoDir }); + await expect(repo.switchBranch('preprod', { reset: true })).to.be.fulfilled; + const branch = await repo.currentBranch(); + expect(branch).to.equal('preprod'); + }); + + it('should sync local branch to remote if switching to existing branch', async () => { + // setup + const priorCommit = '815f77059ce7f968d259af1333b04f2f6d2cab6f'; + const repo = new FluxRepository({ repoDir: testRepoDir }); + await repo.switchBranch('preprod'); + const latestCommit = await repo.currentCommitId(); + await repo.resetToCommitId(priorCommit); + const testCommit = await repo.currentCommitId(); + + // test + expect(testCommit).to.equal(priorCommit); + await repo.switchBranch('master'); + + await repo.switchBranch('preprod'); + const currentCommit = await repo.currentCommitId(); + + expect(currentCommit).to.equal(latestCommit); + }); + + it('should switch to correct branch if multiple remotes', async () => { + // setup + const repoUrl = 'https://github.com/RunOnFlux/flux-integration.git'; + const forkUrl = 'https://github.com/RunOnFlux/flux-integration-fork.git'; + const latestCommit = 'fb4f9097d8b0bc19d0fb901238a4643e72b69398'; + + const repo = new FluxRepository({ repoDir: testRepoDir }); + await repo.addRemote('test_remote', forkUrl); + + // test + const remotes = await repo.remotes(); + const remote = remotes.find( + (r) => r.refs.fetch === repoUrl, + ); + + await repo.switchBranch('preprod', { remote: remote.name }); + + const currentCommit = await repo.currentCommitId(); + + expect(currentCommit).to.equal(latestCommit); + }); +}); diff --git a/tests/unit/fluxRepository.test.js b/tests/unit/fluxRepository.test.js new file mode 100644 index 0000000000..211d845cd8 --- /dev/null +++ b/tests/unit/fluxRepository.test.js @@ -0,0 +1,251 @@ +const chai = require('chai'); +chai.use(require('chai-as-promised')); + +const { expect } = chai; + +const sinon = require('sinon'); + +const os = require('node:os'); +const sg = require('simple-git'); + +const { FluxRepository } = require('../../ZelBack/src/services/utils/fluxRepository'); + +describe('fluxRepository tests', () => { + let getRemotesStub; + let addRemoteStub; + let branchStub; + let fetchStub; + let cleanStub; + let resetStub; + let revparseStub; + let checkoutStub; + let mergeStub; + let gitStub; + beforeEach(async () => { + getRemotesStub = sinon.stub(); + addRemoteStub = sinon.stub(); + branchStub = sinon.stub(); + fetchStub = sinon.stub(); + cleanStub = sinon.stub(); + resetStub = sinon.stub(); + revparseStub = sinon.stub(); + checkoutStub = sinon.stub(); + mergeStub = sinon.stub(); + + gitStub = sinon.stub(sg, 'simpleGit').returns({ + getRemotes: getRemotesStub, + addRemote: addRemoteStub, + branch: branchStub, + fetch: fetchStub, + clean: cleanStub, + reset: resetStub, + revparse: revparseStub, + checkout: checkoutStub, + merge: mergeStub, + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should instantiate and create a git instance with correct properties', () => { + const testOptions = { + baseDir: '/testdir', + binary: 'git', + maxConcurrentProcesses: 6, + trimmed: true, + }; + + expect(() => new FluxRepository({ repoDir: '/testdir' })).to.not.throw(); + + sinon.assert.calledOnceWithExactly(gitStub, testOptions); + }); + + it('should instantiate and use default zelflux dir as baseDir', () => { + sinon.stub(os, 'homedir').returns('/home/testfluxdir'); + + const testOptions = { + baseDir: '/home/testfluxdir/zelflux', + binary: 'git', + maxConcurrentProcesses: 6, + trimmed: true, + }; + + expect(() => new FluxRepository()).to.not.throw(); + + sinon.assert.calledOnceWithExactly(gitStub, testOptions); + }); + + it('should get git remotes in verbose mode', async () => { + const expected = [ + { + name: 'origin', + refs: { + fetch: 'https://github.com/RunOnFlux/flux.git', + push: 'https://github.com/RunOnFlux/flux.git', + }, + }, + ]; + + getRemotesStub.resolves(expected); + + const repo = new FluxRepository({ repoDir: '/test' }); + const remotes = await repo.remotes(); + + sinon.assert.calledOnceWithExactly(getRemotesStub, true); + expect(remotes).to.deep.equal(expected); + }); + + it('should call the underlying addRemote with name and url', async () => { + addRemoteStub.resolves(); + + const repo = new FluxRepository({ repoDir: '/test' }); + await repo.addRemote('test_remote', 'https://blah.com'); + + sinon.assert.calledOnceWithExactly(addRemoteStub, 'test_remote', 'https://blah.com'); + }); + + it('should return the commit id of HEAD on the current branch', async () => { + revparseStub.resolves('12345'); + + const repo = new FluxRepository({ repoDir: '/test' }); + const id = await repo.currentCommitId(); + + sinon.assert.calledOnceWithExactly(revparseStub, 'HEAD'); + expect(id).to.equal('12345'); + }); + + it('should return the current branch', async () => { + branchStub.resolves({ + all: [ + 'master', + 'preprod', + 'remotes/origin/master', + 'remotes/origin/preprod', + ], + branches: { + master: { + current: false, + linkedWorkTree: false, + name: 'master', + commit: '17f91ea', + label: 'Check in new file (ahead of fork)', + }, + preprod: { + current: true, + linkedWorkTree: false, + name: 'preprod', + commit: 'fb4f909', + label: 'Update test_file', + }, + 'remotes/origin/master': { + current: false, + linkedWorkTree: false, + name: 'remotes/origin/master', + commit: '17f91ea', + label: 'Check in new file (ahead of fork)', + }, + 'remotes/origin/preprod': { + current: false, + linkedWorkTree: false, + name: 'remotes/origin/preprod', + commit: 'fb4f909', + label: 'Update test_file', + }, + }, + current: 'preprod', + detached: false, + }); + + const repo = new FluxRepository({ repoDir: '/test' }); + const branch = await repo.currentBranch(); + + sinon.assert.calledOnce(branchStub); + expect(branch).to.equal('preprod'); + }); + + it('should call underlying git reset when resetToId called', async () => { + resetStub.resolves(); + + const expected = [sg.ResetMode.HARD, ['12345']]; + + const repo = new FluxRepository({ repoDir: '/test' }); + await repo.resetToCommitId('12345'); + + sinon.assert.calledWithExactly(resetStub, ...expected); + }); + + it('should track new remote when switchBranch called and it doesn\'t exist locally', async () => { + fetchStub.resolves(); + // local branch doesn't exist + revparseStub.rejects(); + checkoutStub.resolves(); + + const repo = new FluxRepository({ repoDir: '/test' }); + await repo.switchBranch('test_branch'); + + sinon.assert.notCalled(cleanStub); + sinon.assert.notCalled(resetStub); + sinon.assert.calledOnce(fetchStub); + sinon.assert.calledOnce(revparseStub); + sinon.assert.notCalled(mergeStub); + sinon.assert.calledOnceWithExactly(checkoutStub, ['--track', 'origin/test_branch']); + }); + + it('should fetch, then switch to existing branch and fast-forward when switchBranch called and branch exists locally', async () => { + fetchStub.resolves(); + // local branch does exist + revparseStub.resolves('latest commit id here'); + checkoutStub.resolves(); + mergeStub.resolves(); + + const repo = new FluxRepository({ repoDir: '/test' }); + await repo.switchBranch('test_branch'); + + sinon.assert.notCalled(cleanStub); + sinon.assert.notCalled(resetStub); + sinon.assert.calledOnce(fetchStub); + sinon.assert.calledOnce(revparseStub); + sinon.assert.calledOnce(mergeStub); + sinon.assert.calledOnceWithExactly(checkoutStub, 'test_branch'); + }); + + it('should force clean local branch if clean requested on switchBranch', async () => { + fetchStub.resolves(); + // local branch does exist + revparseStub.resolves('latest commit id here'); + checkoutStub.resolves(); + mergeStub.resolves(); + cleanStub.resolves(); + + const repo = new FluxRepository({ repoDir: '/test' }); + await repo.switchBranch('test_branch', { forceClean: true }); + + sinon.assert.calledOnce(fetchStub); + sinon.assert.calledOnce(cleanStub); + sinon.assert.notCalled(resetStub); + sinon.assert.calledOnce(revparseStub); + sinon.assert.calledOnce(mergeStub); + sinon.assert.calledOnceWithExactly(checkoutStub, 'test_branch'); + }); + + it('should reset local branch if reset requested on switchBranch', async () => { + fetchStub.resolves(); + // local branch does exist + revparseStub.resolves('latest commit id here'); + checkoutStub.resolves(); + mergeStub.resolves(); + resetStub.resolves(); + + const repo = new FluxRepository({ repoDir: '/test' }); + await repo.switchBranch('test_branch', { reset: true }); + + sinon.assert.calledOnce(fetchStub); + sinon.assert.notCalled(cleanStub); + sinon.assert.calledOnce(resetStub); + sinon.assert.calledOnce(revparseStub); + sinon.assert.calledOnce(mergeStub); + sinon.assert.calledOnceWithExactly(checkoutStub, 'test_branch'); + }); +}); diff --git a/tests/unit/fluxService.test.js b/tests/unit/fluxService.test.js index 8de9195329..3722b29520 100644 --- a/tests/unit/fluxService.test.js +++ b/tests/unit/fluxService.test.js @@ -27,14 +27,19 @@ const daemonServiceControlRpcs = require('../../ZelBack/src/services/daemonServi const daemonServiceBenchmarkRpcs = require('../../ZelBack/src/services/daemonService/daemonServiceBenchmarkRpcs'); const daemonServiceFluxnodeRpcs = require('../../ZelBack/src/services/daemonService/daemonServiceFluxnodeRpcs'); const daemonServiceBlockchainRpcs = require('../../ZelBack/src/services/daemonService/daemonServiceBlockchainRpcs'); +const daemonServiceUtils = require('../../ZelBack/src/services/daemonService/daemonServiceUtils'); const serviceHelper = require('../../ZelBack/src/services/serviceHelper'); const syncthingService = require('../../ZelBack/src/services/syncthingService'); const packageJson = require('../../package.json'); const adminConfig = require('../../config/userconfig'); +const FluxRepository = function TestClass() { + this.currentBranch = sinon.stub().resolves('master'); +}; + const fluxService = proxyquire( '../../ZelBack/src/services/fluxService', - { '../../../config/userconfig': adminConfig }, + { '../../../config/userconfig': adminConfig, './utils/fluxRepository': { FluxRepository } }, ); const generateResponse = () => { @@ -47,6 +52,10 @@ const generateResponse = () => { }; describe('fluxService tests', () => { + before(async () => { + await daemonServiceUtils.readDaemonConfig(); + }); + describe('fluxBackendFolder tests', () => { afterEach(() => { sinon.restore(); diff --git a/tests/unit/globalconfig/default.js b/tests/unit/globalconfig/default.js index 5661565112..456baf5156 100644 --- a/tests/unit/globalconfig/default.js +++ b/tests/unit/globalconfig/default.js @@ -77,6 +77,12 @@ module.exports = { minimumDockerAllowedVersion: '26.1.2', fluxTeamFluxID: '1NH9BP155Rp3HSf5ef6NpUbE8JcyLRruAM', deterministicNodesStart: 558000, + preProd: { + probability: 0.07, + remote: 'https://github.com/RunOnFlux/flux.git', + branch: 'preprod', + daysToNextEval: 30, + }, fluxapps: { // in flux main chain per month (blocksLasting) price: [