diff --git a/README.md b/README.md index b40a146c6..86184c523 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,6 @@ If you can't access Slack, you can also [subscribe to our mailing list](mailto:g Otherwise, if you have a deeper query or require more support, please [raise an issue](https://github.com/finos/git-proxy/issues) ๐Ÿงต -๐Ÿค Join our [fortnightly Zoom meeting](https://zoom-lfx.platform.linuxfoundation.org/meeting/95849833904?password=99413314-d03a-4b1c-b682-1ede2c399595) on Monday, 4PM BST (odd week numbers). -๐ŸŒ [Convert to your local time](https://www.timeanddate.com/worldclock) +๐Ÿค Join our [fortnightly Zoom meeting](https://zoom-lfx.platform.linuxfoundation.org/meeting/95849833904?password=99413314-d03a-4b1c-b682-1ede2c399595) on Monday, 4PM BST (odd week numbers). +๐ŸŒ [Convert to your local time](https://www.timeanddate.com/worldclock) ๐Ÿ“… [Click here](https://calendar.google.com/calendar/event?action=TEMPLATE&tmeid=MTRvbzM0NG01dWNvNGc4OGJjNWphM2ZtaTZfMjAyNTA2MDJUMTUwMDAwWiBzYW0uaG9sbWVzQGNvbnRyb2wtcGxhbmUuaW8&tmsrc=sam.holmes%40control-plane.io&scp=ALL) for the recurring Google Calendar meeting invite. Alternatively, send an e-mail to [help@finos.org](https://zoom-lfx.platform.linuxfoundation.org/meeting/95849833904?password=99413314-d03a-4b1c-b682-1ede2c399595#:~:text=Need-,an,-invite%3F) to get a calendar invitation. diff --git a/codecov.yml b/codecov.yml index bfdc9877d..b7eed3138 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,7 +2,9 @@ coverage: status: project: default: + target: 80 informational: true patch: default: + target: 80 informational: true diff --git a/nyc.config.js b/nyc.config.js index 7b8a33f43..4f165dfea 100644 --- a/nyc.config.js +++ b/nyc.config.js @@ -1,8 +1,6 @@ const opts = { - branches: 80, + checkCoverage: true, lines: 80, - functions: 80, - statements: 80, }; console.log('nyc config: ', opts); diff --git a/package.json b/package.json index 4ec5d39d3..1c2ec6553 100644 --- a/package.json +++ b/package.json @@ -144,5 +144,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "engines": { + "node": ">=20.19.2" } } diff --git a/test/testProxy.test.js b/test/testProxy.test.js new file mode 100644 index 000000000..d311fbf94 --- /dev/null +++ b/test/testProxy.test.js @@ -0,0 +1,310 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const http = require('http'); +const https = require('https'); +const proxyquire = require('proxyquire'); + +const expect = chai.expect; + +describe('Proxy', () => { + let sandbox; + let Proxy; + let mockHttpServer; + let mockHttpsServer; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + mockHttpServer = { + listen: sandbox.stub().callsFake((port, callback) => { + if (callback) setImmediate(callback); + return mockHttpServer; + }), + close: sandbox.stub().callsFake((callback) => { + if (callback) setImmediate(callback); + return mockHttpServer; + }) + }; + + mockHttpsServer = { + listen: sandbox.stub().callsFake((port, callback) => { + if (callback) setImmediate(callback); + return mockHttpsServer; + }), + close: sandbox.stub().callsFake((callback) => { + if (callback) setImmediate(callback); + return mockHttpsServer; + }) + }; + + sandbox.stub(http, 'createServer').returns(mockHttpServer); + sandbox.stub(https, 'createServer').returns(mockHttpsServer); + + // deep mocking for express router + const mockRouter = sandbox.stub(); + mockRouter.use = sandbox.stub(); + mockRouter.get = sandbox.stub(); + mockRouter.post = sandbox.stub(); + mockRouter.stack = []; + + Proxy = proxyquire('../src/proxy/index', { + './routes': { + getRouter: sandbox.stub().resolves(mockRouter) + }, + '../config': { + getTLSEnabled: sandbox.stub().returns(false), + getTLSKeyPemPath: sandbox.stub().returns('/tmp/key.pem'), + getTLSCertPemPath: sandbox.stub().returns('/tmp/cert.pem'), + getPlugins: sandbox.stub().returns(['mock-plugin']), + getAuthorisedList: sandbox.stub().returns([ + { project: 'test-proj', name: 'test-repo' } + ]) + }, + '../db': { + getRepos: sandbox.stub().resolves([]), + createRepo: sandbox.stub().resolves({ _id: 'mock-repo-id' }), + addUserCanPush: sandbox.stub().resolves(), + addUserCanAuthorise: sandbox.stub().resolves() + }, + '../plugin': { + PluginLoader: sandbox.stub().returns({ + load: sandbox.stub().resolves() + }) + }, + './chain': { + default: {} + }, + '../config/env': { + serverConfig: { + GIT_PROXY_SERVER_PORT: 3000, + GIT_PROXY_HTTPS_SERVER_PORT: 3001 + } + }, + 'fs': { + readFileSync: sandbox.stub().returns(Buffer.from('mock-cert')) + } + }).default; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('start()', () => { + it('should start HTTP server when TLS is disabled', async () => { + const proxy = new Proxy(); + + await proxy.start(); + + expect(http.createServer.calledOnce).to.be.true; + expect(https.createServer.called).to.be.false; + expect(mockHttpServer.listen.calledWith(3000)).to.be.true; + + await proxy.stop(); + }); + + it('should start both HTTP and HTTPS servers when TLS is enabled', async () => { + const mockRouterTLS = sandbox.stub(); + mockRouterTLS.use = sandbox.stub(); + mockRouterTLS.get = sandbox.stub(); + mockRouterTLS.post = sandbox.stub(); + mockRouterTLS.stack = []; + + const ProxyWithTLS = proxyquire('../src/proxy/index', { + './routes': { + getRouter: sandbox.stub().resolves(mockRouterTLS) + }, + '../config': { + getTLSEnabled: sandbox.stub().returns(true), // TLS enabled + getTLSKeyPemPath: sandbox.stub().returns('/tmp/key.pem'), + getTLSCertPemPath: sandbox.stub().returns('/tmp/cert.pem'), + getPlugins: sandbox.stub().returns(['mock-plugin']), + getAuthorisedList: sandbox.stub().returns([]) + }, + '../db': { + getRepos: sandbox.stub().resolves([]), + createRepo: sandbox.stub().resolves({ _id: 'mock-repo-id' }), + addUserCanPush: sandbox.stub().resolves(), + addUserCanAuthorise: sandbox.stub().resolves() + }, + '../plugin': { + PluginLoader: sandbox.stub().returns({ + load: sandbox.stub().resolves() + }) + }, + './chain': { + default: {} + }, + '../config/env': { + serverConfig: { + GIT_PROXY_SERVER_PORT: 3000, + GIT_PROXY_HTTPS_SERVER_PORT: 3001 + } + }, + 'fs': { + readFileSync: sandbox.stub().returns(Buffer.from('mock-cert')) + } + }).default; + + const proxy = new ProxyWithTLS(); + + await proxy.start(); + + expect(http.createServer.calledOnce).to.be.true; + expect(https.createServer.calledOnce).to.be.true; + expect(mockHttpServer.listen.calledWith(3000)).to.be.true; + expect(mockHttpsServer.listen.calledWith(3001)).to.be.true; + + await proxy.stop(); + }); + + it('should set up express app after starting', async () => { + const proxy = new Proxy(); + expect(proxy.getExpressApp()).to.be.null; + + await proxy.start(); + + expect(proxy.getExpressApp()).to.not.be.null; + expect(proxy.getExpressApp()).to.be.a('function'); + + await proxy.stop(); + }); + }); + + describe('getExpressApp()', () => { + it('should return null before start() is called', () => { + const proxy = new Proxy(); + + expect(proxy.getExpressApp()).to.be.null; + }); + + it('should return express app after start() is called', async () => { + const proxy = new Proxy(); + + await proxy.start(); + + const app = proxy.getExpressApp(); + expect(app).to.not.be.null; + expect(app).to.be.a('function'); + expect(app.use).to.be.a('function'); + + await proxy.stop(); + }); + }); + + describe('stop()', () => { + it('should close HTTP server when running', async () => { + const proxy = new Proxy(); + await proxy.start(); + await proxy.stop(); + + expect(mockHttpServer.close.calledOnce).to.be.true; + }); + + it('should close both HTTP and HTTPS servers when both are running', async () => { + const mockRouterStop = sandbox.stub(); + mockRouterStop.use = sandbox.stub(); + mockRouterStop.get = sandbox.stub(); + mockRouterStop.post = sandbox.stub(); + mockRouterStop.stack = []; + + const ProxyWithTLS = proxyquire('../src/proxy/index', { + './routes': { + getRouter: sandbox.stub().resolves(mockRouterStop) + }, + '../config': { + getTLSEnabled: sandbox.stub().returns(true), + getTLSKeyPemPath: sandbox.stub().returns('/tmp/key.pem'), + getTLSCertPemPath: sandbox.stub().returns('/tmp/cert.pem'), + getPlugins: sandbox.stub().returns([]), + getAuthorisedList: sandbox.stub().returns([]) + }, + '../db': { + getRepos: sandbox.stub().resolves([]), + createRepo: sandbox.stub().resolves({ _id: 'mock-repo-id' }), + addUserCanPush: sandbox.stub().resolves(), + addUserCanAuthorise: sandbox.stub().resolves() + }, + '../plugin': { + PluginLoader: sandbox.stub().returns({ + load: sandbox.stub().resolves() + }) + }, + './chain': { + default: {} + }, + '../config/env': { + serverConfig: { + GIT_PROXY_SERVER_PORT: 3000, + GIT_PROXY_HTTPS_SERVER_PORT: 3001 + } + }, + 'fs': { + readFileSync: sandbox.stub().returns(Buffer.from('mock-cert')) + } + }).default; + + const proxy = new ProxyWithTLS(); + await proxy.start(); + await proxy.stop(); + + expect(mockHttpServer.close.calledOnce).to.be.true; + expect(mockHttpsServer.close.calledOnce).to.be.true; + }); + + it('should resolve successfully when no servers are running', async () => { + const proxy = new Proxy(); + + await proxy.stop(); + + expect(mockHttpServer.close.called).to.be.false; + expect(mockHttpsServer.close.called).to.be.false; + }); + + it('should handle errors gracefully', async () => { + const proxy = new Proxy(); + await proxy.start(); + + // simulate error in server close + mockHttpServer.close.callsFake((callback) => { + throw new Error('Server close error'); + }); + + try { + await proxy.stop(); + expect.fail('Expected stop() to reject'); + } catch (error) { + expect(error.message).to.equal('Server close error'); + } + }); + }); + + describe('full lifecycle', () => { + it('should start and stop successfully', async () => { + const proxy = new Proxy(); + + await proxy.start(); + expect(proxy.getExpressApp()).to.not.be.null; + expect(mockHttpServer.listen.calledOnce).to.be.true; + + await proxy.stop(); + expect(mockHttpServer.close.calledOnce).to.be.true; + }); + + it('should handle multiple start/stop cycles', async () => { + const proxy = new Proxy(); + + await proxy.start(); + await proxy.stop(); + + mockHttpServer.listen.resetHistory(); + mockHttpServer.close.resetHistory(); + + await proxy.start(); + await proxy.stop(); + + expect(mockHttpServer.listen.calledOnce).to.be.true; + expect(mockHttpServer.close.calledOnce).to.be.true; + }); + }); +}); diff --git a/website/docs/development/contributing.mdx b/website/docs/development/contributing.mdx index 4b56f7467..54ba6fdc7 100644 --- a/website/docs/development/contributing.mdx +++ b/website/docs/development/contributing.mdx @@ -7,7 +7,7 @@ Here's how to get setup for contributing to GitProxy. ## Setup The GitProxy project relies on the following pre-requisites: -- [Node](https://nodejs.org/en/download) (16+) +- [Node](https://nodejs.org/en/download) (20+) - [npm](https://npmjs.com/) (8+) - [git](https://git-scm.com/downloads) or equivalent Git client. It must support HTTP/S. @@ -26,7 +26,12 @@ $ npm run client # Run only the UI ``` ## Testing - + +Currently, we use Mocha and Chai for unit testing and Cypress for E2E testing. For more details on how to use these testing libraries, check out our [Testing documentation](testing). + +### Patch coverage requirements + +Newly introduced changes **must have over 80% unit test coverage**. This is enforced by our CI, and in practice, only few exceptions (such as emergency fixes) are allowed to skip this requirement. Make sure to add thorough unit tests to your PR to help reviewers approve your PR more quickly! ## Configuration schema The configuration for GitProxy includes a JSON Schema ([`config.schema.json`](https://github.com/finos/git-proxy/blob/main/config.schema.json)) to define the expected properties used by the application. When adding new configuration properties to GitProxy, ensure that the schema is updated with any new, removed or changed properties. See [JSON Schema docs for specific syntax](https://json-schema.org/docs). @@ -43,8 +48,8 @@ When updating the configuration schema, you must also re-generate the reference ## Community Meetings -Join our [fortnightly Zoom meeting](https://zoom-lfx.platform.linuxfoundation.org/meeting/95849833904?password=99413314-d03a-4b1c-b682-1ede2c399595) on Monday, 4PM BST (odd week numbers). -๐ŸŒ [Convert to your local time](https://www.timeanddate.com/worldclock) +Join our [fortnightly Zoom meeting](https://zoom-lfx.platform.linuxfoundation.org/meeting/95849833904?password=99413314-d03a-4b1c-b682-1ede2c399595) on Monday, 4PM BST (odd week numbers). +๐ŸŒ [Convert to your local time](https://www.timeanddate.com/worldclock) [Click here](https://calendar.google.com/calendar/event?action=TEMPLATE&tmeid=MTRvbzM0NG01dWNvNGc4OGJjNWphM2ZtaTZfMjAyNTA2MDJUMTUwMDAwWiBzYW0uaG9sbWVzQGNvbnRyb2wtcGxhbmUuaW8&tmsrc=sam.holmes%40control-plane.io&scp=ALL) for the recurring Google Calendar meeting invite. Alternatively, send an e-mail to [help@finos.org](https://zoom-lfx.platform.linuxfoundation.org/meeting/95849833904?password=99413314-d03a-4b1c-b682-1ede2c399595#:~:text=Need-,an,-invite%3F) to get a calendar invitation. diff --git a/website/docs/development/testing.mdx b/website/docs/development/testing.mdx new file mode 100644 index 000000000..c4de57c45 --- /dev/null +++ b/website/docs/development/testing.mdx @@ -0,0 +1,340 @@ +--- +title: Testing +--- + +## Testing + +As of v1.19.2, GitProxy uses [Mocha](https://mochajs.org/) (`ts-mocha`) as the test runner, and [Chai](https://www.chaijs.com/) for unit test assertions. User interface tests are written in [Cypress](https://docs.cypress.io), and some fuzz testing is done with [`fast-check`](https://fast-check.dev/). + +### Unit testing with Mocha and Chai + +Here's an example unit test that uses Chai for testing (`test/testAuthMethods.test.js`): + +```js +// Import all the test dependencies we need +const chai = require('chai'); +const sinon = require('sinon'); +const proxyquire = require('proxyquire'); + +// Import module that contains the function we want to test +const config = require('../src/config'); + +// Allows using chain-based expect calls +chai.should(); +const expect = chai.expect; + +describe('auth methods', async () => { + it('should return a local auth method by default', async function () { + const authMethods = config.getAuthMethods(); + expect(authMethods).to.have.lengthOf(1); + expect(authMethods[0].type).to.equal('local'); + }); + + it('should return an error if no auth methods are enabled', async function () { + const newConfig = JSON.stringify({ + authentication: [ + { type: 'local', enabled: false }, + { type: 'ActiveDirectory', enabled: false }, + { type: 'openidconnect', enabled: false }, + ], + }); + + const fsStub = { + existsSync: sinon.stub().returns(true), + readFileSync: sinon.stub().returns(newConfig), + }; + + const config = proxyquire('../src/config', { + fs: fsStub, + }); + + // Initialize the user config after proxyquiring to load the stubbed config + config.initUserConfig(); + + expect(() => config.getAuthMethods()).to.throw(Error, 'No authentication method enabled'); + }); + + it('should return an array of enabled auth methods when overridden', async function () { + const newConfig = JSON.stringify({ + authentication: [ + { type: 'local', enabled: true }, + { type: 'ActiveDirectory', enabled: true }, + { type: 'openidconnect', enabled: true }, + ], + }); + + const fsStub = { + existsSync: sinon.stub().returns(true), + readFileSync: sinon.stub().returns(newConfig), + }; + + const config = proxyquire('../src/config', { + fs: fsStub, + }); + + // Initialize the user config after proxyquiring to load the stubbed config + config.initUserConfig(); + + const authMethods = config.getAuthMethods(); + expect(authMethods).to.have.lengthOf(3); + expect(authMethods[0].type).to.equal('local'); + expect(authMethods[1].type).to.equal('ActiveDirectory'); + expect(authMethods[2].type).to.equal('openidconnect'); + }); +}); +``` + +Core concepts to keep in mind when unit testing JS/TS modules with Chai: + +#### Stub internal methods to make tests predictable + +Functions often make use of internal libraries such as `fs` for reading files and performing operations that are dependent on the overall state of the app (or database/filesystem). Since we're only testing that the given function behaves the way we want, we **stub** these libraries. + +For example, here we stub the `fs` library so that "reading" the `proxy.config.json` file returns our mock config file: + +```js +// Define the mock config file +const newConfig = JSON.stringify({ + authentication: [ + { type: 'local', enabled: true }, + { type: 'ActiveDirectory', enabled: true }, + { type: 'openidconnect', enabled: true }, + ], +}); + +// Create the stub for `fs.existsSync` and `fs.readFileSync` +const fsStub = { + existsSync: sinon.stub().returns(true), + readFileSync: sinon.stub().returns(newConfig), +}; +``` + +This stub will make all calls to `fs.existsSync` to return `true` and all calls to `readFileSync` to return the `newConfig` mock file. + +Then, we use `proxyquire` to plug in the stub to the library that we're testing: + +```js +const config = proxyquire('../src/config', { + fs: fsStub, +}); + +// Initialize the user config after proxyquiring to load the stubbed config +config.initUserConfig(); +``` + +Finally, when calling the function we're trying to test, the internal calls will automatically resolve to the values we chose. + +#### Setup and cleanup + +`before` and `beforeEach`, `after` and `afterEach` are testing constructs that allow executing code before and after each test. This allows setting up stubs before each test, making API calls, setting up the database - or otherwise cleaning up the database after test execution. + +This is an example from another test file (`test/addRepoTest.test.js`): + +```js +before(async function () { + app = await service.start(); + + await db.deleteRepo('test-repo'); + await db.deleteUser('u1'); + await db.deleteUser('u2'); + await db.createUser('u1', 'abc', 'test@test.com', 'test', true); + await db.createUser('u2', 'abc', 'test2@test.com', 'test', true); +}); + +// Tests go here + +after(async function () { + await service.httpServer.close(); + + await db.deleteRepo('test-repo'); + await db.deleteUser('u1'); + await db.deleteUser('u2'); +}); + +afterEach(() => { + sinon.restore(); +}); +``` + +Note that `after` will execute once after **all** the tests are complete, whereas `afterEach` will execute at the end of **each** test. + +#### Reset sinon and proxyquire cache + +**It's very important to reset Sinon and the Proxyquire/require cache after each test** when necessary. This prevents old stubs from leaking into subsequent tests. + +Here is an example of a function that resets both of these after each test (`test/chain.test.js`): + +```js +const clearCache = (sandbox) => { + delete require.cache[require.resolve('../src/proxy/processors')]; + delete require.cache[require.resolve('../src/proxy/chain')]; + sandbox.reset(); +}; + +... + +afterEach(() => { + // Clear the module from the cache after each test + clearCache(sandboxSinon); +}); +``` + +#### Focus on expected behaviour + +Mocha and Chai make it easy to write tests in plain English. It's a good idea to write the expected behaviour in plain English and then prove it by writing the test: + +```js +describe('auth methods', async () => { + it('should return a local auth method by default', async function () { + // Test goes here + }); + + it('should return an error if no auth methods are enabled', async function () { + // Test goes here + }); + + it('should return an array of enabled auth methods when overridden', async function () { + // Test goes here + }); +}); +``` + +Assertions can also be done similarly to plain English: + +```js +expect(authMethods).to.have.lengthOf(3); +expect(authMethods[0].type).to.equal('local'); +``` + +#### Unit testing coverage requirement + +**All new lines of code introduced in a PR, must have over 80% coverage** (patch coverage). This is enforced by our CI, and generally a PR will not be merged unless this coverage requirement is met. Please make sure to write thorough unit tests to increase GitProxy's code quality! + +If test coverage is still insufficient after writing your tests, check out the [CodeCov report](https://app.codecov.io/gh/finos/git-proxy) after making the PR and take a look at which lines are missing coverage. + +### UI testing with Cypress + +Although coverage is currently low, we have introduced Cypress testing to make sure that end-to-end flows are working as expected with every added feature. + +This is a sample test from `cypress/e2e/repo.cy.js`: + +```js +describe('Repo', () => { + beforeEach(() => { + // Custom login command + cy.login('admin', 'admin'); + + cy.visit('/dashboard/repo'); + + // prevent failures on 404 request and uncaught promises + cy.on('uncaught:exception', () => false); + }); + + describe('Code button for repo row', () => { + it('Opens tooltip with correct content and can copy', () => { + const cloneURL = 'http://localhost:8000/finos/git-proxy.git'; + const tooltipQuery = 'div[role="tooltip"]'; + + cy + // tooltip isn't open to start with + .get(tooltipQuery) + .should('not.exist'); + + cy + // find the entry for finos/git-proxy + .get('a[href="/dashboard/repo/git-proxy"]') + // take it's parent row + .closest('tr') + // find the nearby span containing Code we can click to open the tooltip + .find('span') + .contains('Code') + .should('exist') + .click(); + + cy + // find the newly opened tooltip + .get(tooltipQuery) + .should('exist') + .find('span') + // check it contains the url we expect + .contains(cloneURL) + .should('exist') + .parent() + // find the adjacent span that contains the svg + .find('span') + .next() + // check it has the copy icon first and click it + .get('svg.octicon-copy') + .should('exist') + .click() + // check the icon has changed to the check icon + .get('svg.octicon-copy') + .should('not.exist') + .get('svg.octicon-check') + .should('exist'); + + // failed to successfully check the clipboard + }); + }); +}); +``` + +Here, we use a similar syntax to Mocha to **describe the behaviour that we expect**. The difference, is that Cypress expects us to write actual commands for executing actions in the app. Some commands used very often include `visit` (navigates to a certain page), `get` (gets a certain page element to check its properties), `contains` (checks if an element has a certain string value in it), `should` (similar to `expect` in unit tests). + +#### Custom commands + +Cypress allows defining **custom commands** to reuse and simplify code. + +In the above example, `cy.login('admin', 'admin')` is actually a custom command defined in `/cypress/support/commands.js`. It allows logging a user into the app, which is a requirement for many E2E flows: + +```js +Cypress.Commands.add('login', (username, password) => { + cy.session([username, password], () => { + cy.visit('/login'); + cy.intercept('GET', '**/api/auth/me').as('getUser'); + + cy.get('[data-test=username]').type(username); + cy.get('[data-test=password]').type(password); + cy.get('[data-test=login]').click(); + + cy.wait('@getUser'); + cy.url().should('include', '/dashboard/repo'); + }); +}); +``` + +### Fuzz testing with fast-check + +Fuzz testing helps find edge case bugs by generating random inputs for test data. This is very helpful since regular tests often have naive assumptions of users always inputting "expected" data. + +Fuzz testing with fast-check is very easy: it integrates seamlessly with Mocha and it doesn't require any additional libraries beyond fast-check itself. + +Here's an example of a fuzz test section for a test file (`testCheckRepoInAuthList.test.js`): + +```js +const fc = require('fast-check'); + +// Unit tests go here + +describe('fuzzing', () => { + it('should not crash on random repo names', async () => { + await fc.assert( + fc.asyncProperty( + fc.string(), + async (repoName) => { + const action = new actions.Action('123', 'type', 'get', 1234, repoName); + const result = await processor.exec(null, action, authList); + expect(result.error).to.be.true; + } + ), + { numRuns: 100 } + ); + }); +}); +``` + +Writing fuzz tests is a bit different from regular unit tests, although we do still `assert` whether a certain value is correct or not. In this example, fc.string() indicates that a random string value is being generated for the `repoName` variable. This `repoName` is then inserted in the `action` to see if the `processor.exec()` function is capable of handling these or not. + +In this case, we expect that the `result.error` value is always true. This means that the `exec` flow always errors out, but never crashes the app entirely. You may also want to test that the app is always able to complete a flow without an error. + +Finally, we have the `numRuns` property for `fc.assert`. This allows us to run the fuzz test multiple times with a new randomized value each time. This is important since the test may randomly fail or pass depending on the input. diff --git a/website/sidebars.js b/website/sidebars.js index 6573101d1..c778eca85 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -43,7 +43,7 @@ module.exports = { }, collapsible: true, collapsed: false, - items: ['development/contributing', 'development/plugins'], + items: ['development/contributing', 'development/plugins', 'development/testing'], }, ], };