diff --git a/.github/helper/.gitignore b/.github/helper/.gitignore new file mode 100644 index 00000000..9132f3bd --- /dev/null +++ b/.github/helper/.gitignore @@ -0,0 +1 @@ +.nyc_output/ diff --git a/.github/helper/package.json b/.github/helper/package.json index 5f88a010..0cc06607 100644 --- a/.github/helper/package.json +++ b/.github/helper/package.json @@ -1,5 +1,19 @@ { + "name": "node-minecraft-protocol-helper", + "version": "1.0.0", + "description": "Helper scripts for node-minecraft-protocol automation", + "main": "updator.js", + "scripts": { + "test": "mocha test/**/*.test.js", + "test:watch": "mocha test/**/*.test.js --watch", + "test:coverage": "nyc mocha test/**/*.test.js" + }, + "devDependencies": { + "mocha": "^10.2.0", + "sinon": "^17.0.1", + "nyc": "^15.1.0" + }, "dependencies": { - "gh-helpers": "^1.0.0" + "gh-helpers": "*" } } \ No newline at end of file diff --git a/.github/helper/test/updator.test.js b/.github/helper/test/updator.test.js new file mode 100644 index 00000000..09bdb166 --- /dev/null +++ b/.github/helper/test/updator.test.js @@ -0,0 +1,188 @@ +const sinon = require('sinon') +const fs = require('fs') +const cp = require('child_process') +const assert = require('assert') + +// Mock gh-helpers +const mockGithub = { + mock: true, + createPullRequest: sinon.stub().resolves({ number: 123, url: 'test-pr' }) +} + +// Mock modules +const Module = require('module') +const originalRequire = Module.prototype.require + +Module.prototype.require = function(id) { + if (id === 'gh-helpers') { + return () => mockGithub + } + return originalRequire.apply(this, arguments) +} + +describe('Node Minecraft Protocol Updator', function() { + let originalEnv + let fsStub + let cpStub + + beforeEach(function() { + originalEnv = process.env + process.env = { ...originalEnv } + + // Stub fs and child_process + fsStub = { + readFileSync: sinon.stub(fs, 'readFileSync'), + writeFileSync: sinon.stub(fs, 'writeFileSync') + } + + cpStub = { + execSync: sinon.stub(cp, 'execSync') + } + + sinon.reset() + }) + + afterEach(function() { + process.env = originalEnv + sinon.restore() + }) + + describe('Version Update', function() { + it('should add new version to supportedVersions array', function() { + const currentVersionFile = `'use strict' + +module.exports = { + defaultVersion: '1.21.8', + supportedVersions: ['1.7', '1.8.8', '1.21.8'] +}` + + fsStub.readFileSync.returns(currentVersionFile) + + // Test the logic for adding new version + const newContents = currentVersionFile.replace(", '1.21.8'", ", '1.21.8', '1.21.9'") + assert(newContents.includes("'1.21.9'"), 'Should contain new version') + }) + + it('should not duplicate existing versions', function() { + const versionFileWithExisting = `module.exports = { + supportedVersions: ['1.21.6', '1.21.8'] +}` + + fsStub.readFileSync.returns(versionFileWithExisting) + + // Should not add duplicate + const result = versionFileWithExisting.includes('1.21.8') + ? versionFileWithExisting + : versionFileWithExisting.replace("]", ", '1.21.8']") + + assert.strictEqual(result, versionFileWithExisting, 'Should not change if version exists') + }) + + it('should update README.md with new version', function() { + const readmeContent = `# Minecraft Protocol + +Supports Minecraft 1.8 to 1.21.8 (https://wiki.vg/Protocol_version_numbers) + +Versions 1.7.10, 1.8.8, 1.21.6, 1.21.8) ` + + const expectedReadme = readmeContent + .replace('Minecraft 1.8 to 1.21.8 (', 'Minecraft 1.8 to 1.21.9 (') + .replace(') ', ', 1.21.9) ') + + assert(expectedReadme.includes('1.21.9'), 'README should contain new version') + assert(expectedReadme.includes('Minecraft 1.8 to 1.21.9'), 'README should update version range') + }) + }) + + describe('Git Operations', function() { + it('should create correct branch name', function() { + const version = '1.21.9' + const expectedBranch = 'pc' + version.replace(/[^a-zA-Z0-9_]/g, '_') + assert.strictEqual(expectedBranch, 'pc1_21_9') + }) + + it('should execute git commands in correct order', function() { + const expectedCommands = [ + 'git checkout -b pc1_21_9', + 'git config user.name "github-actions[bot]"', + 'git config user.email "41898282+github-actions[bot]@users.noreply.github.com"', + 'git add --all', + 'git commit -m "Update to version 1.21.9"', + 'git push origin pc1_21_9 --force' + ] + + // Verify command sequence would be correct + expectedCommands.forEach((cmd, index) => { + assert(cmd.includes('git'), `Command ${index} should be a git command`) + }) + }) + }) + + describe('Environment Variable Validation', function() { + it('should fail without required NEW_MC_VERSION', function() { + delete process.env.NEW_MC_VERSION + + assert.throws(() => { + const newVersion = process.env.NEW_MC_VERSION?.replace(/[^a-zA-Z0-9_.]/g, '_') + if (!newVersion) throw new Error('NEW_MC_VERSION required') + }, /NEW_MC_VERSION required/) + }) + + it('should sanitize version strings correctly', function() { + const testCases = [ + { input: '1.21.9', expected: '1.21.9' }, + { input: '1.21.9-test', expected: '1.21.9_test' }, + { input: '24w01a', expected: '24w01a' }, + { input: 'invalid!@#', expected: 'invalid___' } + ] + + testCases.forEach(({ input, expected }) => { + const sanitized = input.replace(/[^a-zA-Z0-9_.]/g, '_') + assert.strictEqual(sanitized, expected) + }) + }) + }) + + describe('PR Creation', function() { + it('should create PR with correct title and body', function() { + const version = '1.21.9' + const expectedTitle = `🎈 ${version}` + const expectedBody = `This automated PR sets up the relevant boilerplate for Minecraft version ${version}. + +Ref: + +* You can help contribute to this PR by opening a PR against this pc1_21_9 branch instead of master. + ` + + assert.strictEqual(expectedTitle, '🎈 1.21.9') + assert(expectedBody.includes('Minecraft version 1.21.9'), 'Body should contain version') + assert(expectedBody.includes('pc1_21_9'), 'Body should contain branch name') + }) + }) + + describe('Error Handling', function() { + it('should handle git command failures', function() { + cpStub.execSync.throws(new Error('Push failed')) + + assert.throws(() => { + try { + cpStub.execSync('git push origin test --force') + } catch (e) { + throw new Error(`Git operation failed: ${e.message}`) + } + }, /Git operation failed: Push failed/) + }) + + it('should handle file system errors', function() { + fsStub.readFileSync.throws(new Error('File not found')) + + assert.throws(() => { + try { + fsStub.readFileSync('non-existent-file') + } catch (e) { + throw new Error(`File operation failed: ${e.message}`) + } + }, /File operation failed: File not found/) + }) + }) +}) \ No newline at end of file