diff --git a/.eslintignore b/.eslintignore index b9400c5..961cda6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,4 @@ node_modules .vscode .sfdx out +coverage diff --git a/.eslintrc.js b/.eslintrc.js index a69d457..afd5cf9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,18 +1,26 @@ module.exports = { - "env": { - "browser": true, - "es6": true + env: { + browser: true, + es6: true, + }, + extends: 'airbnb-base', + globals: { + Atomics: 'readonly', + SharedArrayBuffer: 'readonly', + }, + parserOptions: { + ecmaVersion: 2020, + }, + rules: { + 'no-unused-vars': 1, + 'no-param-reassign': ['error', { props: false }], + }, + overrides: [ + { + files: ['tests/**/*.js', '__mocks__/**/*.js'], + env: { + jest: true, + }, }, - "extends": "airbnb-base", - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parserOptions": { - "ecmaVersion": 2018 - }, - "rules": { - "no-unused-vars": 1, - "no-param-reassign": ["error", { "props": false }] - } + ], }; diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e359e64..1030752 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -10,7 +10,10 @@ ElectronForce is an [Electron](https://www.electronjs.org/)-based desktop applic electronForce/ ├── main.js # Electron main process: window creation, app lifecycle, IPC handler registration ├── src/ -│ └── electronForce.js # Core backend logic: JSForce connection management and IPC handler definitions +│ ├── electronForce.js # Core backend logic: JSForce connection management and IPC handler definitions +│ └── settings.js # Read/write persistent user settings via app.getPath('userData')/settings.json +├── __mocks__/ +│ └── electron.js # Jest manual mock for the electron module (shared across all test files) ├── app/ │ ├── index.html # Application UI markup │ ├── render.js # Renderer process logic: UI interaction and IPC send/receive calls @@ -21,6 +24,11 @@ electronForce/ ├── .github/ │ ├── workflows/ # CI workflows: lint.yml and codeql-analysis.yml │ └── ISSUE_TEMPLATE/ # Bug report and feature request templates +├── tests/ +│ ├── electronForce.test.js # Tests for src/electronForce.js IPC handlers +│ ├── handlers.test.js # Tests for individual handler logic +│ ├── render.test.js # Tests for app/render.js renderer logic +│ └── settings.test.js # Tests for src/settings.js read/write logic ├── .husky/ │ └── pre-commit # Runs lint and tests before every commit ├── .eslintrc.js # ESLint configuration (airbnb-base) @@ -39,10 +47,15 @@ ElectronForce follows the standard Electron two-process model with strict securi ### IPC Channel Convention -- Channels sent **from the renderer to the main process** use the prefix `sf_` (e.g., `sf_login`, `sf_query`) or `get_` (e.g., `get_log_messages`). -- Channels sent **from the main process back to the renderer** use the prefix `response_` (e.g., `response_login`, `response_query`, `response_generic`). +- Channels sent **from the renderer to the main process** use the prefix `sf_` (e.g., `sf_oauth_start`, `sf_query`) or `get_` (e.g., `get_log_messages`). Note: `sf_oauth_start` is the current authentication entry point; `sf_login` is no longer used. +- Settings persistence uses `sf_get_settings` and `sf_save_settings`; the main process responds on `response_settings`. +- Channels sent **from the main process back to the renderer** use the prefix `response_` (e.g., `response_query`, `response_generic`, `response_settings`). - Every new handler added to `src/electronForce.js` must also be added to the `validChannels` allow-list in `app/preload.js`. +### Authentication + +Authentication uses Salesforce OAuth 2.0 via a Salesforce External Client App. The main process opens the authorization URL in the system browser via `shell.openExternal`. A one-time local HTTP server (default port 3835, configurable in settings) receives the callback, exchanges the auth code for tokens using `jsforce.OAuth2`, and stores the resulting connection in `sfConnections`. The local server closes after a successful exchange or a 5-minute timeout. + ### Response Payload Shape All IPC responses follow a consistent structure: @@ -67,6 +80,7 @@ This project follows the guidelines described in `contributing.md`. Key points: - **Security**: Never enable `nodeIntegration`, `enableRemoteModule`, or disable `contextIsolation` in `BrowserWindow` settings. Keep the preload channel allow-lists up to date. - **No unused variables**: The ESLint rule `no-unused-vars` is set to `warn`. Treat warnings as errors before opening a pull request. - **Parameter reassignment**: Direct property mutations on function parameters are allowed (`"props": false`), but avoid reassigning the parameter binding itself. +- **Settings mock**: Any test file that imports or indirectly loads `src/electronForce.js` must include `jest.mock('../src/settings')` to prevent real file-system reads/writes during testing. ## Testing @@ -75,6 +89,7 @@ Tests are written with [Jest](https://jestjs.io/) and live alongside the source - After any session that modifies code beyond comments, ensure the full test suite passes and linting is clean before considering the work done. - When adding new IPC handlers or utility functions, add corresponding Jest tests. - Tests for the main-process logic in `src/electronForce.js` should mock `jsforce` and the `mainWindow` object to avoid requiring a live Electron or Salesforce environment. +- Any test file that needs Electron APIs (e.g., `app.getPath`) should call `jest.mock('electron')` (no factory). Jest will automatically use the shared manual mock at `__mocks__/electron.js`, which returns a temp-directory path for `app.getPath('userData')` and exposes `shell.openExternal` as a `jest.fn()` so tests can assert on it without triggering real browser calls. Do not duplicate the mock factory inline in individual test files. - The `--passWithNoTests` flag is used for the pre-commit hook so that new files without tests don't block commits, but coverage is expected for substantive logic. ## Contribution Expectations diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 19e8237..d679a09 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,12 +13,12 @@ name: "CodeQL" on: push: - branches: [ main ] + branches: [main] pull_request: # The branches below must be a subset of the branches above - branches: [ main ] + branches: [main] schedule: - - cron: '41 8 * * 4' + - cron: "41 8 * * 4" jobs: analyze: @@ -32,40 +32,40 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'javascript' ] + language: ["javascript"] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v4 - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language - #- run: | - # make bootstrap - # make release + #- run: | + # make bootstrap + # make release - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..877e39e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,28 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + # The branches below must be a subset of the branches above + branches: [main] + +jobs: + test: + name: Tests + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm test diff --git a/ReadMe.md b/ReadMe.md index c8e6cee..408c929 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -15,17 +15,37 @@ From your terminal: npm install npm start -ElectronForce will allow you to log into your Salesforce Org and interact with some of the APIs. While all of JSForce's supported APIs are listed, only Query, Search, and Describe are currently support. +ElectronForce uses OAuth to connect to your Salesforce Org and interact with the APIs. While all of JSForce's supported APIs are listed, only Query, Search, and Describe are currently supported. -### Log in +### Setup: Create a Salesforce External Client App -Currently only the standard login is supported, not OAuth2, so you likely will need your [security token](https://help.salesforce.com/articleView?id=user_security_token.htm&type=5). +Before connecting ElectronForce to a Salesforce org you need to create an External Client App to generate OAuth credentials: -In the login fields provide your username, password, and security token. If you are logging into a production or trailhead instance you can use the default login URL. If you are logging into a Sandbox use: https://test.salesforce.com. +1. In Salesforce, navigate to **Setup → App Manager** (use Quick Find if needed). +2. Click **New External Client App**. +3. Enter a **Name** and accept or edit the generated **API Name**. +4. Enter a **Contact Email**. +5. Set the **Distribution State** to **Local** (for use only in this org). +6. Save the app, then edit it and enable **OAuth**. +7. In the OAuth settings, set the **Callback URL** to `http://localhost:3835/callback`. +8. Add the following **OAuth Scopes**: `api` and `refresh_token`. +9. Save. Copy the **Consumer Key** (Client ID) and **Consumer Secret** from the OAuth detail page. + +For full details see the Salesforce Help article [Create an External Client App](https://help.salesforce.com/s/articleView?id=xcloud.create_a_local_external_client_app.htm&type=5). + +### Configure ElectronForce + +Open the **Settings** modal in ElectronForce and enter the **Consumer Key** and **Consumer Secret** from your External Client App. If you are connecting to a sandbox org, set the **Login URL** to `https://test.salesforce.com`. For production and Trailhead orgs the default login URL (`https://login.salesforce.com`) can be used. + +> **Security note:** The Consumer Key and Consumer Secret are encrypted at rest using [`electron.safeStorage`](https://www.electronjs.org/docs/latest/api/safe-storage), which delegates to the OS-level credential store (Keychain on macOS, the secret service on Linux, DPAPI on Windows). On platforms where OS-level encryption is unavailable, the values fall back to plain text in the application's user-data directory. + +### Connect to Salesforce + +Click the **Connect via OAuth** button. ElectronForce will open the Salesforce login page in your default browser. After you approve access, Salesforce redirects to the local callback URL and ElectronForce completes the OAuth handshake automatically. ![ElectronForce Main screen.](./documentation/images/ElectronForceMain.png "Login fields as described above and query API example as follows.") -The main interface includes the login information, API selector and parameter fields on the left, raw display of the previous API response on the right, and a processed version of the response at the bottom. +The main interface includes the API selector and parameter fields on the left, raw display of the previous API response on the right, and a processed version of the response at the bottom. ### Run Query or Search diff --git a/__mocks__/electron.js b/__mocks__/electron.js new file mode 100644 index 0000000..e47f66f --- /dev/null +++ b/__mocks__/electron.js @@ -0,0 +1,18 @@ +const os = require('os'); +const path = require('path'); + +module.exports = { + app: { + getPath: jest.fn(() => path.join(os.tmpdir(), 'electronforce-test')), + }, + shell: { + openExternal: jest.fn(), + }, + safeStorage: { + isEncryptionAvailable: jest.fn(() => true), + // Passthrough: encode the string into a Buffer so round-trips work in tests. + encryptString: jest.fn((str) => Buffer.from(str, 'utf8')), + // Passthrough: decode the Buffer back to the original string. + decryptString: jest.fn((buf) => buf.toString('utf8')), + }, +}; diff --git a/app/index.html b/app/index.html index bb78322..7a37503 100644 --- a/app/index.html +++ b/app/index.html @@ -55,67 +55,63 @@

This is a Salesforce API interface build using JSForce and Electron. It allows basic interactions with Salesforce remote APIs to assist development and testing.

- + + -