├── .all-contributorsrc ├── .codacy.yaml ├── .compactignore ├── .complexrc ├── .eslintrc.js ├── .gcloudignore ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── dependabot.yml ├── renovate.json └── workflows │ ├── ci-standards.yml │ ├── ci-tests-pull.yml │ ├── ci-tests.yml │ └── codeql.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── SECURITY.md ├── app.example.yaml ├── codeql-config.yml ├── dispatch.yaml ├── docs ├── build.md ├── index.md ├── reference │ ├── API_Definition.md │ ├── Admin_Actions.md │ ├── Source_Documentation.md │ ├── assets │ │ ├── pulsar-oauth-screen.png │ │ └── pulsar-website-sign-up-page.png │ ├── auth.md │ ├── badge_spec.md │ ├── bubbled_errors.md │ ├── database.md │ ├── glossary.md │ ├── how-the-backend-was-built.md │ ├── logging.md │ ├── manage_pulsar_account.md │ ├── numeric_error_codes.md │ ├── packages.md │ ├── queries.md │ ├── returns.md │ ├── server_status_object.md │ ├── structures.md │ └── style_guide.md ├── resources │ ├── ORIGINAL_README.md │ ├── complexity-report.md │ ├── featured_packages.json │ ├── featured_themes.json │ ├── jsdoc_typedef.js │ ├── name_ban_list.json │ ├── oauth_app_config.yaml │ ├── package_object_full.json │ ├── package_object_short.json │ └── refactor-docs.md ├── swagger │ ├── index.html │ └── openapi3_def.yaml └── writingIntegrationTests.md ├── jest.config.js ├── jsdoc.conf.js ├── package-lock.json ├── package.json ├── scripts ├── database │ ├── create_authstate_table.sql │ ├── create_names_table.sql │ ├── create_packages_table.sql │ ├── create_stars_table.sql │ ├── create_users_table.sql │ └── create_versions_table.sql ├── deprecated-migrations │ ├── 0002-post-star-test.sql │ ├── 0003-post-package-version-test.sql │ ├── 0004-delete-package-test.sql │ ├── 0005-get-user-test.sql │ ├── 0006-get-stars-test.sql │ ├── 0007-get-package.sql │ └── 0008-migrated-initial.sql ├── deprecated │ ├── README.md │ ├── debug_utils.js │ ├── search.js │ ├── search.test.js │ ├── searchWithinPackages.js │ └── test │ │ ├── README.md │ │ ├── delete.packages.handler.integration.test.js │ │ ├── get.packages.handler.integration.test.js │ │ ├── handlers │ │ ├── common_handler │ │ │ ├── auth_fail.test.js │ │ │ ├── bad_package_json.test.js │ │ │ ├── bad_repo_json.test.js │ │ │ ├── express_fakes.js │ │ │ ├── handle_detailed_error.test.js │ │ │ ├── missing_auth_json.test.js │ │ │ ├── not_found.test.js │ │ │ ├── not_supported.test.js │ │ │ ├── package_exists.test.js │ │ │ ├── server_error.test.js │ │ │ └── site_wide_not_found.test.js │ │ ├── get_package_handler │ │ │ ├── getPackages.test.js │ │ │ └── getPackagesSearch.test.js │ │ └── post_package_handler │ │ │ └── postPackages.test.js │ │ ├── oauth.handler.integration.test.js │ │ ├── themes.handler.integration.test.js │ │ └── users.handler.integration.test.js ├── migrations │ └── 0001-initial-migration.sql ├── openapi │ ├── header-map.js │ └── main.js └── tools │ ├── .gitignore │ ├── add-badge.js │ ├── add-owner-field.js │ ├── feature-detection.js │ ├── genBadges.js │ ├── get-licenses.js │ ├── health-check.js │ ├── manual-delete-package.js │ ├── search.js │ └── update-bundled.js ├── src ├── ServerStatusObject.js ├── auth.js ├── bundled_packages │ ├── bundled.json │ └── index.js ├── cache.js ├── config.js ├── context.js ├── controllers │ ├── deletePackagesPackageName.js │ ├── deletePackagesPackageNameStar.js │ ├── deletePackagesPackageNameVersionsVersionName.js │ ├── endpoints.js │ ├── getLogin.js │ ├── getOauth.js │ ├── getOwnersOwnerName.js │ ├── getPackages.js │ ├── getPackagesFeatured.js │ ├── getPackagesPackageName.js │ ├── getPackagesPackageNameStargazers.js │ ├── getPackagesPackageNameVersionsVersionName.js │ ├── getPackagesPackageNameVersionsVersionNameTarball.js │ ├── getPackagesSearch.js │ ├── getPat.js │ ├── getRoot.js │ ├── getStars.js │ ├── getThemes.js │ ├── getThemesFeatured.js │ ├── getThemesSearch.js │ ├── getUpdates.js │ ├── getUsers.js │ ├── getUsersLogin.js │ ├── getUsersLoginStars.js │ ├── postPackages.js │ ├── postPackagesPackageNameStar.js │ ├── postPackagesPackageNameVersions.js │ └── postPackagesPackageNameVersionsVersionNameEventsUninstall.js ├── database │ ├── _clause.js │ ├── _constants.js │ ├── _export.js │ ├── _utils.js │ ├── applyFeatures.js │ ├── authCheckAndDeleteStateKey.js │ ├── authStoreStateKey.js │ ├── getFeaturedPackages.js │ ├── getFeaturedThemes.js │ ├── getPackageByName.js │ ├── getPackageByNameSimple.js │ ├── getPackageCollectionByID.js │ ├── getPackageCollectionByName.js │ ├── getPackageVersionByNameAndVersion.js │ ├── getSortedPackages.js │ ├── getStarredPointersByUserID.js │ ├── getStarringUsersByPointer.js │ ├── getUserByID.js │ ├── getUserByName.js │ ├── getUserByNodeID.js │ ├── getUserCollectionById.js │ ├── insertNewPackage.js │ ├── insertNewPackageName.js │ ├── insertNewPackageVersion.js │ ├── insertNewUser.js │ ├── packageNameAvailability.js │ ├── removePackageByName.js │ ├── removePackageVersion.js │ ├── updateDecrementStar.js │ ├── updateIncrementStar.js │ ├── updatePackageDecrementDownloadByName.js │ ├── updatePackageIncrementDownloadByName.js │ └── updatePackageStargazers.js ├── dev_server.js ├── logger.js ├── models │ ├── callStack.js │ ├── constructNewPackagePublishData.js │ ├── constructPackageObjectFull.js │ ├── constructPackageObjectJSON.js │ ├── constructPackageObjectShort.js │ ├── sso.js │ ├── ssoHTML.js │ ├── ssoPaginate.js │ └── ssoRedirect.js ├── query_parameters │ ├── auth.js │ ├── direction.js │ ├── engine.js │ ├── fileExtension.js │ ├── filter.js │ ├── index.js │ ├── login.js │ ├── owner.js │ ├── ownerName.js │ ├── packageName.js │ ├── page.js │ ├── query.js │ ├── rename.js │ ├── repository.js │ ├── service.js │ ├── serviceType.js │ ├── serviceVersion.js │ ├── shared │ │ └── owner.js │ ├── sort.js │ ├── tag.js │ ├── utils.js │ └── versionName.js ├── server.js ├── setupEndpoints.js ├── storage.js ├── utils.js ├── vcs.js ├── vcs_providers │ ├── README.md │ ├── git.js │ └── github.js └── webhook.js └── tests ├── database ├── applyFeatures.test.js ├── database.test.js ├── extensionFilter.test.js └── fixtures │ ├── git.createPackage_returns │ ├── valid_multi_version.js │ └── valid_one_version.js │ └── lifetime │ ├── package-a.js │ └── user-a.js ├── full ├── fixtures │ ├── a-pulsar-package │ │ ├── match.js │ │ ├── package.json │ │ ├── readme.md │ │ └── tags.js │ ├── b-pulsar-package │ │ ├── match.js │ │ ├── package.json │ │ ├── readme.md │ │ └── tags.js │ ├── c-pulsar-package │ │ ├── package.json │ │ ├── readme.md │ │ └── tags.js │ └── d-pulsar-package │ │ ├── initial-package.json │ │ ├── initial-tags.js │ │ ├── match.js │ │ ├── package.json │ │ ├── readme.md │ │ └── tags.js ├── publish-version.test.js └── publish.test.js ├── helpers ├── global.setup.jest.js ├── handlers.setup.jest.js ├── httpMock.helper.jest.js └── package.jest.js ├── http ├── deletePackagesPackageName.test.js ├── deletePackagesPackageNameVersionsVersionName.test.js ├── getOwnersOwnerName.test.js ├── getPackages.test.js ├── getPackagesFeatured.test.js ├── getPackagesPackageName.test.js ├── getPackagesSearch.test.js ├── getRoot.test.js ├── getThemes.test.js ├── getThemesFeatured.test.js ├── getThemesSearch.test.js ├── getUpdates.test.js ├── getUsers.test.js ├── getUsersLogin.test.js ├── login.test.js ├── options.test.js ├── postPackages.test.js ├── postPackagesPackageNameStar.test.js ├── postPackagesPackageNameVersions.test.js └── stars.test.js ├── models ├── message.js ├── packageObjectFull.js ├── packageObjectFullArray.js ├── packageObjectJSON.js ├── packageObjectShort.js ├── packageObjectShortArray.js ├── userObjectPrivate.js ├── userObjectPublic.js └── userObjectPublicArray.js ├── unit ├── ServerStatusObject.test.js ├── cache.test.js ├── config.test.js ├── controllers │ ├── deletePackagesPackageName.test.js │ ├── deletePackagesPackageNameStar.js │ ├── deletePackagesPackageNameVersionsVersionName.test.js │ ├── getRoot.test.js │ ├── getStars.test.js │ ├── getThemes.test.js │ ├── getThemesSearch.test.js │ ├── getUpdates.test.js │ ├── getUsers.test.js │ ├── getUsersLogin.test.js │ ├── postPackagesPackageNameVersions.test.js │ └── postPackagesPackageNameVersionsVersionNameEventsUninstall.test.js ├── endpoints.test.js ├── logger.test.js ├── models │ ├── callStack.test.js │ ├── constructPackageObjectFull.test.js │ ├── constructPackageObjectJSON.test.js │ └── constructPackageObjectShort.test.js ├── query.test.js ├── storage.test.js ├── utils.test.js └── webhook.test.js └── vcs ├── github.vcs.test.js ├── vcs.unit.test.js └── vcs.vcs.test.js /.codacy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | minTokenMatch: 80 5 | exclude_paths: 6 | - "./docs/*" 7 | - "./scripts/**/**" 8 | -------------------------------------------------------------------------------- /.compactignore: -------------------------------------------------------------------------------- 1 | > GLOBAL 2 | node_modules/ 3 | # Ignore old JSON based artifact 4 | data/ 5 | bundled-repos/ 6 | 7 | > gitignore 8 | # Ignore our secrets 9 | app.yaml 10 | pulsar-*.json 11 | ca-certificate.crt 12 | # Ignore unneeded files 13 | coverage/ 14 | 15 | > gcloudignore 16 | .gcloudignore 17 | test/ 18 | tests/ 19 | scripts/ 20 | docs/reference 21 | docs/resources/ 22 | docs/build.md 23 | docs/host_your_own.md 24 | docs/writingIntegrationTests.md 25 | # Git Tooling / NPM Tooling 26 | .git/ 27 | .github/ 28 | .gitattributes 29 | .gitignore 30 | codql-config.yml 31 | # Community Health Files 32 | ARCHITECTURE.md 33 | CHANGELOG.md 34 | CODE_OF_CONDUCT.md 35 | CONTRIBUTING.md 36 | FUNDING.yml 37 | LICENSE.md 38 | PULL_REQUEST_TEMPLATE.md 39 | README.md 40 | SECURITY.md 41 | # Other configs 42 | .all-contributorsrc 43 | .codacy.yaml 44 | .compactignore 45 | .complexrc 46 | .eslintrc.js 47 | .nvmrc 48 | .prettierignore 49 | app.example.yaml 50 | codeql-config.yml 51 | jest.config.js 52 | jsdoc.conf.js 53 | 54 | > prettierignore 55 | *.md 56 | coverage/ 57 | # Ignore Git tooling 58 | .git/ 59 | .github/ 60 | .gitignore 61 | .gitattributes 62 | -------------------------------------------------------------------------------- /.complexrc: -------------------------------------------------------------------------------- 1 | { 2 | "excludepattern": "node_modules", 3 | "filepattern": "\\.js$", 4 | "output": "./docs/resources/complexity-report.md", 5 | "format": "markdown", 6 | "ignoreerrors": true 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es2021: true, 6 | }, 7 | extends: ["eslint:recommended", "plugin:node/recommended"], 8 | overrides: [], 9 | parserOptions: { 10 | ecmaVersion: "latest", 11 | }, 12 | rules: { 13 | "node/no-unpublished-require": [ 14 | "error", 15 | { 16 | allowModules: ["supertest"], 17 | }, 18 | ], 19 | "no-process-exit": "off", 20 | // Custom Rules as Determined by the maintainers of package-backend 21 | complexity: ["error"], 22 | eqeqeq: ["error", "smart"], 23 | "max-depth": ["error", 4], 24 | camelcase: "off", 25 | }, 26 | plugins: [], 27 | globals: { 28 | jest: "readonly", 29 | test: "readonly", 30 | expect: "readonly", 31 | describe: "readonly", 32 | beforeAll: "readonly", 33 | beforeEach: "readonly", 34 | afterEach: "readonly", 35 | afterAll: "readonly", 36 | process: "writeable", 37 | Buffer: "readonly", 38 | Joi: "readonly", 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | # === Autogenerated by CompactIgnore::gcloudignore === # 2 | 3 | node_modules/ 4 | # Ignore old JSON based artifact 5 | data/ 6 | bundled-repos/ 7 | .gcloudignore 8 | test/ 9 | tests/ 10 | scripts/ 11 | docs/reference 12 | docs/resources/ 13 | docs/build.md 14 | docs/host_your_own.md 15 | docs/writingIntegrationTests.md 16 | # Git Tooling / NPM Tooling 17 | .git/ 18 | .github/ 19 | .gitattributes 20 | .gitignore 21 | codql-config.yml 22 | # Community Health Files 23 | ARCHITECTURE.md 24 | CHANGELOG.md 25 | CODE_OF_CONDUCT.md 26 | CONTRIBUTING.md 27 | FUNDING.yml 28 | LICENSE.md 29 | PULL_REQUEST_TEMPLATE.md 30 | README.md 31 | SECURITY.md 32 | # Other configs 33 | .all-contributorsrc 34 | .codacy.yaml 35 | .compactignore 36 | .complexrc 37 | .eslintrc.js 38 | .nvmrc 39 | .prettierignore 40 | app.example.yaml 41 | codeql-config.yml 42 | jest.config.js 43 | jsdoc.conf.js 44 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a Bug or Issue 4 | title: "[BUG] **Summary of Issue**" 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **What is the Bug** 10 | A clear and concise description of the problem at hand. 11 | 12 | **How to Replicate the Bug** 13 | A step by step guide to reproducing the issue. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "**Summary of New Feature**" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | timezone: "America/Los_Angeles" 13 | ignore: 14 | - dependency-name: "@databases/pg-migrations" 15 | - dependency-name: "@databases/pg-test" 16 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "config:base", ":dependencyDashboardApproval"], 3 | "constraints": { 4 | "node": "< 18" 5 | }, 6 | "labels": ["dependencies"], 7 | "separateMajorMinor": "false" 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/ci-standards.yml: -------------------------------------------------------------------------------- 1 | # GitHub Action to include all documentation that needs to generated, and all linting 2 | # that needs to be done into a single PR. 3 | 4 | name: CI - Standards 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | 10 | jobs: 11 | standards: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | 16 | strategy: 17 | matrix: 18 | node-version: [18.x] 19 | 20 | steps: 21 | - name: Checkout the latest code 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup NodeJS - ${{ matrix.node-version }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | 30 | - name: Install Dependencies 31 | run: npm install 32 | 33 | - name: Lint Codebase 34 | run: npm run lint 35 | 36 | - name: Generate JSDocs 37 | run: npm run js-docs 38 | 39 | - name: Generate Complexity Reports 40 | run: npm run complex 41 | 42 | - name: Generate OpenAPI Docs 43 | run: npm run openapi-docs 44 | 45 | - name: Commit All Changes 46 | uses: stefanzweifel/git-auto-commit-action@v4 47 | with: 48 | commit_message: GH Action Standards 49 | -------------------------------------------------------------------------------- /.github/workflows/ci-tests-pull.yml: -------------------------------------------------------------------------------- 1 | # Single GitHub Action to run all types of tests. But only runs on a PR, and does 2 | # not commit any changes, nor upload them to coveralls 3 | name: CI - Tests PR 4 | 5 | on: 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | env: 10 | # Setup all Environment Variables as tests check they exist. 11 | PORT: 8080 12 | SERVERURL: "https://test.github.com" 13 | PAGINATE: 30 14 | CACHETIME: 600000 15 | GCLOUD_STORAGE_BUCKET: "" 16 | GOOGLE_APPLICATION_CREDENTIALS: "nofile" 17 | GH_CLIENTID: "" 18 | GH_CLIENTSECRET: "" 19 | GH_USERAGENT: "Pulsar-Edit Tester Bot" 20 | GH_REDIRECTURI: "http://localhost:8080/api/oauth" 21 | DB_HOST: "" 22 | DB_USER: "" 23 | DB_PASS: "" 24 | DB_DB: "" 25 | DB_PORT: 43 26 | DB_SSL_CERT: "" 27 | LOG_LEVEL: 6 28 | LOG_FORMAT: "stdout" 29 | # This helps some modules know they are in a test environment 30 | NODE_ENV: "test" 31 | PULSAR_STATUS: "dev" 32 | RATE_LIMIT_GENERIC: 0 33 | RATE_LIMIT_AUTH: 0 34 | WEBHOOK_PUBLISH: "" 35 | WEBHOOK_VERSION: "" 36 | WEBHOOK_USERNAME: "" 37 | 38 | 39 | jobs: 40 | tests: 41 | runs-on: ubuntu-latest 42 | permissions: 43 | contents: write 44 | 45 | strategy: 46 | matrix: 47 | node-version: [17.x, 18.x, 19.x] 48 | 49 | steps: 50 | - name: Checkout the latest code 51 | uses: actions/checkout@v4 52 | 53 | - name: Setup NodeJS - ${{ matrix.node-version }} 54 | uses: actions/setup-node@v4 55 | with: 56 | node-version: ${{ matrix.node-version }} 57 | cache: 'npm' 58 | 59 | - name: Install Dependencies 60 | run: npm install 61 | 62 | - name: Run Tests 63 | run: npm run test 64 | -------------------------------------------------------------------------------- /.github/workflows/ci-tests.yml: -------------------------------------------------------------------------------- 1 | # Single GitHub Action to run tests against the codebase 2 | # This test includes integration tests and unit tests all in one. 3 | name: CI - Tests 4 | 5 | on: 6 | workflow_dispatch: 7 | workflow_run: 8 | workflows: [ "CI - Standards" ] 9 | types: 10 | - completed 11 | # Using this to prevent simultaneous runs, outdating local git cache 12 | 13 | env: 14 | # Setup all Environment Variables as tests check they exist. 15 | PORT: 8080 16 | SERVERURL: "https://test.github.com" 17 | PAGINATE: 30 18 | CACHETIME: 600000 19 | GCLOUD_STORAGE_BUCKET: "" 20 | GOOGLE_APPLICATION_CREDENTIALS: "nofile" 21 | GH_CLIENTID: "" 22 | GH_CLIENTSECRET: "" 23 | GH_USERAGENT: "Pulsar-Edit Tester Bot" 24 | GH_REDIRECTURI: "http://localhost:8080/api/oauth" 25 | DB_HOST: "" 26 | DB_USER: "" 27 | DB_PASS: "" 28 | DB_DB: "" 29 | DB_PORT: 43 30 | DB_SSL_CERT: "" 31 | LOG_LEVEL: 6 32 | LOG_FORMAT: "stdout" 33 | # This helps some modules know they are in a test environment 34 | NODE_ENV: "test" 35 | PULSAR_STATUS: "dev" 36 | RATE_LIMIT_GENERIC: 0 37 | RATE_LIMIT_AUTH: 0 38 | WEBHOOK_PUBLISH: "" 39 | WEBHOOK_VERSION: "" 40 | WEBHOOK_USERNAME: "" 41 | 42 | jobs: 43 | tests: 44 | runs-on: ubuntu-latest 45 | permissions: 46 | contents: write 47 | 48 | strategy: 49 | matrix: 50 | node-version: [17.x, 18.x, 19.x] 51 | 52 | steps: 53 | - name: Checkout the latest code 54 | uses: actions/checkout@v4 55 | 56 | - name: Setup NodeJS - ${{ matrix.node-version }} 57 | uses: actions/setup-node@v4 58 | with: 59 | node-version: ${{ matrix.node-version }} 60 | cache: 'npm' 61 | 62 | - name: Install Dependencies 63 | run: npm install 64 | 65 | - name: Run Tests 66 | run: npm run test 67 | 68 | - name: Codecov Upload 69 | if: always() # Needed to ensure coverage is commited even on test failure 70 | uses: codecov/codecov-action@v3 71 | with: 72 | files: ./coverage/clover.xml 73 | 74 | #- name: Coveralls Upload 75 | #if: always() # Needed to ensure coverage is commited even on test failure 76 | #uses: coverallsapp/github-action@master 77 | #with: 78 | #github-token: ${{ secrets.GITHUB_TOKEN }} 79 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '41 2 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v4 40 | 41 | # Initializes the CodeQL tools for scanning. 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@v2 44 | with: 45 | config-file: ./codeql-config.yml 46 | languages: ${{ matrix.language }} 47 | # If you wish to specify custom queries, you can do so here or in a config file. 48 | # By default, queries listed here will override any specified in a config file. 49 | # Prefix the list here with "+" to use these queries and those in the config file. 50 | 51 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 52 | # queries: security-extended,security-and-quality 53 | 54 | - name: Perform CodeQL Analysis 55 | uses: github/codeql-action/analyze@v2 56 | with: 57 | category: "/language:${{matrix.language}}" 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # === Autogenerated by CompactIgnore::gitignore === # 2 | 3 | node_modules/ 4 | # Ignore old JSON based artifact 5 | data/ 6 | bundled-repos/ 7 | # Ignore our secrets 8 | app.yaml 9 | pulsar-*.json 10 | ca-certificate.crt 11 | # Ignore unneeded files 12 | coverage/ 13 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # === Autogenerated by CompactIgnore::prettierignore === # 2 | 3 | node_modules/ 4 | # Ignore old JSON based artifact 5 | data/ 6 | bundled-repos/ 7 | *.md 8 | coverage/ 9 | # Ignore Git tooling 10 | .git/ 11 | .github/ 12 | .gitignore 13 | .gitattributes 14 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: confusedtechie 2 | open_collective: pulsar-edit 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 confused_techie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Requirements 2 | 3 | * Filling out the template is required. 4 | * All new code requires tests to ensure against regressions. 5 | - However, if your PR contains zero code changes, feel free to select the checkmark below to indicate so. 6 | 7 | * [ ] Have you ran tests against this code? 8 | * [ ] This PR contains zero code changes. 9 | 10 | ### Description of the Change 11 | 12 | 16 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | < 1.0.0 | :white_check_mark: | 8 | | 1.0.0 | :white_check_mark: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | If you encounter or discover a vulnerability within `package-backend` please feel free to report it. 13 | To report a vulnerability feel free to contact the developers any of the following ways: 14 | * [Submit a Private Security Vulnerability](https://github.com/pulsar-edit/package-backend/security/advisories/new) 15 | * [Pulsar Discord Server](https://discord.gg/7aEbB9dGRT) 16 | * [Email the Pulsar Team](mailto:admin@pulsar-edit.dev) 17 | * [Create a PR with a Fix](https://github.com/pulsar-edit/package-backend/pulls) 18 | * [Create an Issue](https://github.com/pulsar-edit/package-backend/issues) 19 | 20 | Any security vulnerabilities reported will be meet with extreme care to resolve it. Updates to fix said vulnerability 21 | will be released as quickly as possible while considering this is a 100% community and volunteer project. 22 | 23 | Thank you for taking the time to consider assisting with a security issue. 24 | -------------------------------------------------------------------------------- /app.example.yaml: -------------------------------------------------------------------------------- 1 | # To use this configuration, copy its contents to a new file at the root of the project. 2 | # Name this file app.yaml, and it will be picked up by the server. 3 | # app.yaml is in the git ignore to prevent any secrets from leaking. 4 | 5 | runtime: nodejs18 6 | service: default 7 | 8 | env_variables: 9 | PORT: 8080 10 | # Valid Search: 11 | SERVERURL: "http://localhost:8080" 12 | # Paginate is the amount to paginate results by. 13 | PAGINATE: 30 14 | # The cache time defines how long until an in memory resource is expired, and demands to 15 | # be read from disk again. Measured in milliseconds. 16 | # The default value for testing is 10 minutes = 600000 17 | CACHETIME: 600000 18 | # The following settings are for storing data within Google Cloud Storage 19 | GCLOUD_STORAGE_BUCKET: "bucket_name" 20 | GOOGLE_APPLICATION_CREDENTIALS: "local_file_path" 21 | # The GH Client ID, for use with OAuth 22 | GH_CLIENTID: "" 23 | # The Client Secret to accompany the CLIENT ID 24 | GH_CLIENTSECRET: "" 25 | # The User Agent thats used to communicate with GitHub 26 | GH_USERAGENT: "Pulsar-Edit Bot" 27 | # The URI that the OAuth instance should redirect to. Should end in `/api/oauth` 28 | GH_REDIRECTURI: "https://api.pulsar-edit.dev/api/oauth" 29 | # The following are Database settings that should be pretty self-explainitory. 30 | DB_HOST: "" 31 | DB_USER: "" 32 | DB_PASS: "" 33 | DB_DB: "" 34 | DB_PORT: 8080 35 | # This should be the path to a cert file for your DB 36 | DB_SSL_CERT: "" 37 | # Determines how verbose your logs will be. 38 | # Requires a numeric value to be input. These below values are inclusive of values below it. 39 | # 1 - Fatal 40 | # 2 - Error 41 | # 3 - Warning 42 | # 4 - Information 43 | # 5 - Debug 44 | # 6 - Trace 45 | LOG_LEVEL: 6 46 | # Determines how Logs are written. Currently supports the following options: 47 | # stdout - Writes the console 48 | LOG_FORMAT: "stdout" 49 | RATE_LIMIT_GENERIC: 600 50 | RATE_LIMIT_AUTH: 300 51 | WEBHOOK_PUBLISH: "" 52 | WEBHOOK_VERSION: "" 53 | WEBHOOK_USERNAME: "" 54 | -------------------------------------------------------------------------------- /codeql-config.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL Config" 2 | 3 | queries: 4 | - uses: security-extended 5 | - uses: security-and-quality 6 | 7 | paths-ignore: 8 | - ./test 9 | - ./tests 10 | - ./scripts/deprecated/test 11 | # https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors 12 | -------------------------------------------------------------------------------- /dispatch.yaml: -------------------------------------------------------------------------------- 1 | dispatch: 2 | # The Package Browser Service handles all `web` subdomain root requests 3 | - url: "web.pulsar-edit.dev/" 4 | service: package-browser 5 | 6 | # Package Browser Service handles root `web` subdomain requests with paths 7 | - url: "web.pulsar-edit.dev/*" 8 | service: package-browser 9 | 10 | # Default (This) Service handles `api` root requests 11 | - url: "api.pulsar-edit.dev/" 12 | service: default 13 | 14 | # Default (This) Service handles `api` subdomain requests with paths 15 | - url: "api.pulsar-edit.dev/*" 16 | service: default 17 | -------------------------------------------------------------------------------- /docs/reference/assets/pulsar-oauth-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulsar-edit/package-backend/e1d13f76b2dee1db50bc2ba69c86a1182411e237/docs/reference/assets/pulsar-oauth-screen.png -------------------------------------------------------------------------------- /docs/reference/assets/pulsar-website-sign-up-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulsar-edit/package-backend/e1d13f76b2dee1db50bc2ba69c86a1182411e237/docs/reference/assets/pulsar-website-sign-up-page.png -------------------------------------------------------------------------------- /docs/reference/how-the-backend-was-built.md: -------------------------------------------------------------------------------- 1 | # How the Backend Was Built 2 | 3 | When The term "reverse-engineer" is used throughout the documentation to describe how the backend was built, it's good form to have a longer explanation here as well as cover the Pulsar Team from any legal questions that may stem from this terms use. 4 | 5 | The process to build the backend was not true reverse-engineering it was more in fact building a compatible system for the Pulsar Editor, with the purpose to create a backend that did not require any major changes to the editor to work. 6 | 7 | Whether this goal was achieved in it's entirety it's completely to be seen. 8 | 9 | But at it's core the process used was to hit every endpoint of the (at the time) existing Atom.io Package Registry and determine how it seemed to process the query. For example what would fail first in a request the Authentication or a non-existent package. Or what errors would you receive to certain requests. 10 | 11 | This process was then used to shape how the backend was originally planned and created but even during the creation process had to differ because of our likely varied technologies and tech stack used to create the backend. 12 | 13 | Beyond this the code used to create the backend is unique, and is the work of the editors that have committed to the repo, which then falls under the MIT license. 14 | -------------------------------------------------------------------------------- /docs/reference/server_status_object.md: -------------------------------------------------------------------------------- 1 | # Server Status Object's 2 | 3 | The way the backend communicates between the many different modules, and retains context is via Server Status Objects. 4 | 5 | A Server Status Object is what is returned after nearly every descrete action, and allows vital and complex communication between modules. 6 | 7 | While some of this functionality could be accomplished by errors, the idea is to never have to error catch an interaction with a module, and that the module itself will be fully self reliant in terms of errors, instead returning an object that can be checked for success. 8 | 9 | The basic format of these: 10 | 11 | * ok: The boolean status to indicate if a request was successful. 12 | * short: The recognized enum of valid failure types. 13 | * content: A detailed error message, that is safe to display to users. 14 | * error: An optional error value from a stack trace that can be added to the server status object. 15 | -------------------------------------------------------------------------------- /docs/resources/featured_packages.json: -------------------------------------------------------------------------------- 1 | [ 2 | "tidalcycles", 3 | "language-dart2", 4 | "sb-atom-sonic-pi", 5 | "x-terminal-reloaded", 6 | "dracula-syntax", 7 | "atom-clock", 8 | "hey-pane", 9 | "tidal-sharp", 10 | "language-tiger" 11 | ] 12 | -------------------------------------------------------------------------------- /docs/resources/featured_themes.json: -------------------------------------------------------------------------------- 1 | ["atom-material-ui", "atom-material-syntax"] 2 | -------------------------------------------------------------------------------- /docs/resources/name_ban_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | "atom-pythoncompiler", 3 | "situs-slot-gacor", 4 | "slot-online-gacor", 5 | "slot88", 6 | "slot-gacor-hari-ini", 7 | "demo-slot-joker-gacor", 8 | "hoki-slot", 9 | "slot-bonus-new-member", 10 | "slot-dana", 11 | "slot-deposit-dana-100000", 12 | "slot-deposit-pulsa", 13 | "slot-hoki", 14 | "slot-paling-gacor-setiap-hari", 15 | "slot-pulsa", 16 | "slothoki", 17 | "slotonline", 18 | "ui-theme-template", 19 | "active-editor-info", 20 | "ascii-art", 21 | "your-name-word-count", 22 | "another-user-theme", 23 | "out-dated-1", 24 | "out-dated-2", 25 | "user-package", 26 | "first", 27 | "not-first", 28 | "test-package", 29 | "test-package-a", 30 | "test-package-b", 31 | "test-package-c", 32 | "core-theme", 33 | "core-package", 34 | "user-theme", 35 | "dev-theme", 36 | "dev-package" 37 | ] 38 | -------------------------------------------------------------------------------- /docs/resources/oauth_app_config.yaml: -------------------------------------------------------------------------------- 1 | # There is no actual Config Available for this data 2 | # But in the spirit of full documentation this will be added in a made up format. 3 | 4 | client_id: "" 5 | application_logo: 6 | file: pulsar_discord.png 7 | background_color: "#fff" 8 | application_name: "Pulsar" 9 | homepage_url: "https://pulsar-edit.dev" 10 | application_description: "A Community-led Hyper-Hackable Text Editor" 11 | callback_url: "https://api.pulsar-edit.dev/api/oauth" 12 | enable_device_flow: false 13 | -------------------------------------------------------------------------------- /docs/swagger/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |Server is up and running Version ${context.server_version}
24 | Swagger UI 25 | Documentation 26 | `; 27 | 28 | const sso = new context.ssoHTML(); 29 | 30 | return sso.isOk().addContent(str); 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/controllers/getThemes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module getThemes 3 | */ 4 | 5 | module.exports = { 6 | docs: { 7 | summary: "List all packages that are themes.", 8 | responses: { 9 | 200: { 10 | description: "A paginated response of themes.", 11 | content: { 12 | "application/json": "$packageObjectShortArray", 13 | }, 14 | }, 15 | }, 16 | }, 17 | endpoint: { 18 | method: "GET", 19 | paths: ["/api/themes"], 20 | rateLimit: "generic", 21 | successStatus: 200, 22 | options: { 23 | Allow: "POST, GET", 24 | "X-Content-Type-Options": "nosniff", 25 | }, 26 | }, 27 | params: { 28 | page: (context, req) => { 29 | return context.query.page(req); 30 | }, 31 | sort: (context, req) => { 32 | return context.query.sort(req); 33 | }, 34 | direction: (context, req) => { 35 | return context.query.direction(req); 36 | }, 37 | }, 38 | 39 | /** 40 | * @async 41 | * @memberOf getThemes 42 | * @function logic 43 | * @desc Returns all themes to the user. Based on any filters they've applied 44 | * via query parameters. 45 | * @returns {object} ssoPaginate 46 | */ 47 | async logic(params, context) { 48 | const packages = await context.database.getSortedPackages(params, true); 49 | 50 | if (!packages.ok) { 51 | const sso = new context.sso(); 52 | 53 | return sso 54 | .notOk() 55 | .addContent(packages) 56 | .addCalls("db.getSortedPackages", packages); 57 | } 58 | 59 | const packObjShort = await context.models.constructPackageObjectShort( 60 | packages.content 61 | ); 62 | 63 | const packArray = Array.isArray(packObjShort) 64 | ? packObjShort 65 | : [packObjShort]; 66 | 67 | const ssoP = new context.ssoPaginate(); 68 | 69 | ssoP.resultCount = packages.pagination.count; 70 | ssoP.totalPages = packages.pagination.total; 71 | ssoP.limit = packages.pagination.limit; 72 | ssoP.buildLink( 73 | `${context.config.server_url}/api/themes`, 74 | packages.pagination.page, 75 | params 76 | ); 77 | 78 | return ssoP.isOk().addContent(packArray); 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /src/controllers/getThemesFeatured.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module getThemesFeatured 3 | */ 4 | 5 | module.exports = { 6 | docs: { 7 | summary: "Display featured packages that are themes.", 8 | responses: { 9 | 200: { 10 | description: "An array of featured themes.", 11 | content: { 12 | "application/json": "$packageObjectShortArray", 13 | }, 14 | }, 15 | }, 16 | }, 17 | endpoint: { 18 | method: "GET", 19 | paths: ["/api/themes/featured"], 20 | rateLimit: "generic", 21 | successStatus: 200, 22 | options: { 23 | Allow: "GET", 24 | "X-Content-Type-Options": "nosniff", 25 | }, 26 | }, 27 | params: { 28 | // Currently we don't seem to utilize any query parameters here. 29 | // We likely want to make this match whatever is used in getPackagesFeatured.js 30 | }, 31 | async logic(params, context) { 32 | const col = await context.database.getFeaturedThemes(); 33 | 34 | if (!col.ok) { 35 | const sso = new context.sso(); 36 | 37 | return sso.notOk().addContent(col).addCalls("db.getFeaturedThemes", col); 38 | } 39 | 40 | const newCol = await context.models.constructPackageObjectShort( 41 | col.content 42 | ); 43 | 44 | const sso = new context.sso(); 45 | 46 | return sso.isOk().addContent(newCol); 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/controllers/getThemesSearch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module getThemesSearch 3 | */ 4 | 5 | module.exports = { 6 | docs: { 7 | summary: "Get featured packages that are themes. Previously undocumented.", 8 | responses: { 9 | 200: { 10 | description: "A paginated response of themes.", 11 | content: { 12 | "application/json": "$packageObjectShortArray", 13 | }, 14 | }, 15 | }, 16 | }, 17 | endpoint: { 18 | method: "GET", 19 | paths: ["/api/themes/search"], 20 | rateLimit: "generic", 21 | successStatus: 200, 22 | options: { 23 | Allow: "GET", 24 | "X-Content-Type-Options": "nosniff", 25 | }, 26 | }, 27 | params: { 28 | sort: (context, req) => { 29 | return context.query.sort(req); 30 | }, 31 | page: (context, req) => { 32 | return context.query.page(req); 33 | }, 34 | direction: (context, req) => { 35 | return context.query.direction(req); 36 | }, 37 | query: (context, req) => { 38 | return context.query.query(req); 39 | }, 40 | }, 41 | 42 | async logic(params, context) { 43 | const packs = await context.database.getSortedPackages(params, true); 44 | 45 | if (!packs.ok) { 46 | const sso = new context.sso(); 47 | 48 | return sso 49 | .notOk() 50 | .addContent(packs) 51 | .addCalls("db.getSortedPackages", packs); 52 | } 53 | 54 | const newPacks = await context.models.constructPackageObjectShort( 55 | packs.content 56 | ); 57 | 58 | let packArray = null; 59 | 60 | if (Array.isArray(newPacks)) { 61 | packArray = newPacks; 62 | } else if (Object.keys(newPacks).length < 1) { 63 | packArray = []; 64 | } else { 65 | packArray = [newPacks]; 66 | } 67 | 68 | const ssoP = new context.ssoPaginate(); 69 | 70 | ssoP.total = packs.pagination.total; 71 | ssoP.limit = packs.pagination.limit; 72 | ssoP.buildLink( 73 | `${context.config.server_url}/api/themes/search`, 74 | packs.pagination.page, 75 | params 76 | ); 77 | 78 | return ssoP.isOk().addContent(packArray); 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /src/controllers/getUpdates.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module getUpdates 3 | */ 4 | 5 | module.exports = { 6 | docs: { 7 | summary: "List Pulsar Updates", 8 | description: 9 | "Currently returns 'Not Implemented' as Squirrel AutoUpdate is not supported.", 10 | responses: { 11 | 200: { 12 | description: 13 | "Atom update feed, following the format expected by Squirrel.", 14 | content: {}, 15 | }, 16 | }, 17 | }, 18 | endpoint: { 19 | method: "GET", 20 | paths: ["/api/updates"], 21 | rateLimit: "generic", 22 | successStatus: 200, 23 | options: { 24 | Allow: "GET", 25 | "X-Content-Type-Options": "nosniff", 26 | }, 27 | }, 28 | params: {}, 29 | 30 | /** 31 | * @async 32 | * @memberof getUpdates 33 | * @function logic 34 | * @desc Used to retrieve new editor update information. 35 | * @todo This function has never been implemented within Pulsar. 36 | */ 37 | async logic(params, context) { 38 | const sso = new context.sso(); 39 | 40 | return sso.notOk().addShort("not_supported"); 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/controllers/getUsersLogin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module getUsersLogin 3 | */ 4 | 5 | module.exports = { 6 | docs: { 7 | summary: 8 | "Display the details of any user, as well as the packages they have published.", 9 | responses: { 10 | 200: { 11 | description: "The public details of a specific user.", 12 | content: { 13 | // This references the file name of a `./tests/models` model 14 | "application/json": "$userObjectPublic", 15 | }, 16 | }, 17 | 404: { 18 | description: "The User requested cannot be found.", 19 | content: { 20 | "application/json": "$message", 21 | }, 22 | }, 23 | }, 24 | }, 25 | endpoint: { 26 | method: "GET", 27 | paths: ["/api/users/:login"], 28 | rateLimit: "generic", 29 | successStatus: 200, 30 | options: { 31 | Allow: "GET", 32 | "X-Content-Type-Options": "nosniff", 33 | }, 34 | }, 35 | params: { 36 | login: (context, req) => { 37 | return context.query.login(req); 38 | }, 39 | }, 40 | 41 | /** 42 | * @async 43 | * @memberOf getUserLogin 44 | * @desc Returns the user account details of another user. Including all 45 | * packages published. 46 | */ 47 | async logic(params, context) { 48 | let user = await context.database.getUserByName(params.login); 49 | 50 | if (!user.ok) { 51 | const sso = new context.sso(); 52 | 53 | return sso.notOk().addContent(user).addCalls("db.getUserByName", user); 54 | } 55 | 56 | // TODO We need to find a way to add the users published pacakges here 57 | // When we do we will want to match the schema in ./docs/returns.md#userobjectfull 58 | // Until now we will return the public details of their account. 59 | 60 | // Although now we have a user to return, but we need to ensure to strip any 61 | // sensitive details since this return will go to any user. 62 | const returnUser = { 63 | username: user.content.username, 64 | avatar: user.content.avatar, 65 | created_at: `${user.content.created_at}`, 66 | data: user.content.data ?? {}, 67 | packages: [], // included as it should be used in the future 68 | }; 69 | 70 | const sso = new context.sso(); 71 | 72 | return sso.isOk().addContent(returnUser); 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /src/controllers/postPackagesPackageNameStar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module postPackagesPackageNameStar 3 | */ 4 | 5 | module.exports = { 6 | docs: { 7 | summary: "Star a package.", 8 | responses: { 9 | 200: { 10 | description: "A 'Package Object Full' of the modified package.", 11 | content: { 12 | "application/json": "$packageObjectFull", 13 | }, 14 | }, 15 | }, 16 | }, 17 | endpoint: { 18 | method: "POST", 19 | paths: ["/api/packages/:packageName/star", "/api/themes/:packageName/star"], 20 | rateLimit: "auth", 21 | successStatus: 200, 22 | options: { 23 | Allow: "DELETE, POST", 24 | "X-Content-Type-Options": "nosniff", 25 | }, 26 | }, 27 | params: { 28 | auth: (context, req) => { 29 | return context.query.auth(req); 30 | }, 31 | packageName: (context, req) => { 32 | return context.query.packageName(req); 33 | }, 34 | }, 35 | async logic(params, context) { 36 | const callStack = new context.callStack(); 37 | 38 | const user = await context.auth.verifyAuth(params.auth, context.database); 39 | 40 | callStack.addCall("auth.verifyAuth", user); 41 | 42 | if (!user.ok) { 43 | const sso = new context.sso(); 44 | 45 | return sso.notOk().addContent(user).assignCalls(callStack); 46 | } 47 | 48 | const star = await context.database.updateIncrementStar( 49 | user.content, 50 | params.packageName 51 | ); 52 | 53 | callStack.addCall("db.updateIncrementStar", star); 54 | 55 | if (!star.ok) { 56 | const sso = new context.sso(); 57 | 58 | return sso.notOk().addContent(star).assignCalls(callStack); 59 | } 60 | 61 | // Now with a success we want to return the package back in this query 62 | let pack = await context.database.getPackageByName( 63 | params.packageName, 64 | true 65 | ); 66 | 67 | callStack.addCall("db.getPackageByName", pack); 68 | 69 | if (!pack.ok) { 70 | const sso = new context.sso(); 71 | 72 | return sso.notOk().addContent(pack).assignCalls(callStack); 73 | } 74 | 75 | pack = await context.models.constructPackageObjectFull(pack.content); 76 | 77 | const sso = new context.sso(); 78 | 79 | return sso.isOk().addContent(pack); 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /src/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module postPackagesPackageNameVersionsVersionNameEventsUninstall 3 | */ 4 | 5 | module.exports = { 6 | docs: { 7 | summary: "Previously undocumented endpoint. Since v1.0.2 has no effect.", 8 | deprecated: true, 9 | responses: { 10 | 201: { 11 | description: 12 | "A generic message indicating success, included only for backwards compatibility.", 13 | content: {}, 14 | }, 15 | }, 16 | }, 17 | endpoint: { 18 | method: "POST", 19 | paths: [ 20 | "/api/packages/:packageName/versions/:versionName/events/uninstall", 21 | "/api/themes/:packageName/versions/:versionName/events/uninstall", 22 | ], 23 | rateLimit: "auth", 24 | successStatus: 201, 25 | options: { 26 | Allow: "POST", 27 | "X-Content-Type-Options": "nosniff", 28 | }, 29 | }, 30 | params: {}, 31 | async logic(params, context) { 32 | /** 33 | Used when a package is uninstalled, decreases the download count by 1. 34 | Originally an undocumented endpoint. 35 | The decision to return a '201' is based on how other POST endpoints return, 36 | during a successful event. 37 | This endpoint has now been deprecated, as it serves no useful features, 38 | and on further examination may have been intended as a way to collect 39 | data on users, which is not something we implement. 40 | * Deprecated since v1.0.2 41 | * see: https://github.com/atom/apm/blob/master/src/uninstall.coffee 42 | * While decoupling HTTP handling from logic, the function has been removed 43 | entirely: https://github.com/pulsar-edit/package-backend/pull/171 44 | */ 45 | 46 | const sso = new context.sso(); 47 | 48 | return sso.isOk().addContent({ ok: true }); 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /src/database/_constants.js: -------------------------------------------------------------------------------- 1 | // Constants that relate to the database modules 2 | 3 | module.exports = { 4 | defaults: { 5 | engine: { atom: "*" }, 6 | license: "NONE", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/database/_utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function getOrderField 4 | * @description Internal method to parse the sort method and return the related database field/column. 5 | * @param {string} method - The sort method. 6 | * @param {object} sqlStorage - The database class instance used parse the proper field. 7 | * @returns {object|null} The string field associated to the sort method or null if the method is not recognized. 8 | */ 9 | function getOrderField(method, sql) { 10 | switch (method) { 11 | case "relevance": 12 | case "downloads": 13 | return sql`downloads`; 14 | case "created_at": 15 | return sql`created`; 16 | case "updated_at": 17 | return sql`updated`; 18 | case "stars": 19 | return sql`stargazers_count`; 20 | default: 21 | return null; 22 | } 23 | } 24 | 25 | module.exports = { 26 | getOrderField, 27 | }; 28 | -------------------------------------------------------------------------------- /src/database/authCheckAndDeleteStateKey.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function authCheckAndDeleteStateKey 4 | * @desc Gets a state key from oauth process and delete it from the database. 5 | * It's used to verify if the request for the authentication is valid. The code should be first generated in the 6 | * initial stage of the login and then deleted by this function. 7 | * If the deletion is successful, the returned record is used to retrieve the created timestamp of the state key 8 | * and check if it's not expired (considering a specific timeout). 9 | * A custom timestamp can be passed as argument for testing purpose, otherwise the current timestamp is considered. 10 | * @param {string} stateKey - The key code string to delete. 11 | * @param {string} timestamp - A string in SQL timestamp format to check against the created timestamp of the 12 | * given state key. If not provided, the current UNIX timestamp is used. 13 | * @returns {object} A server status object. 14 | */ 15 | 16 | module.exports = { 17 | safe: false, 18 | exec: async (sql, stateKey, timestamp = null) => { 19 | // We need to compare the created timestamp with Date.now() timestamp which 20 | // is returned in milliseconds. 21 | // We convert the created SQL timestamp to UNIX timestamp which has an 22 | // high resolution up to microseconds, but it's returned in seconds with a 23 | // fractional part. 24 | // So we first convert it to milliseconds, then cast to BIGINT to remove the 25 | // unneeded remaining fractional part. 26 | // BIGINT has to be used because ms UNIX timestamp does not fit into 27 | // PostgreSQL INT type. 28 | const command = await sql` 29 | DELETE FROM authstate 30 | WHERE keycode = ${stateKey} 31 | RETURNING keycode, CAST(extract(epoch from created) * 1000 AS BIGINT) AS created; 32 | `; 33 | 34 | if (command.count === 0) { 35 | return { 36 | ok: false, 37 | content: "The provided state key was not set for the auth login.", 38 | short: "not_found", 39 | }; 40 | } 41 | 42 | const created = BigInt(command[0].created); 43 | const timeout = 600000n; // 10*60*1000 => 10 minutes in ms 44 | const now = timestamp ?? Date.now(); 45 | 46 | if (now > created + timeout) { 47 | return { 48 | ok: false, 49 | content: "The provided state key is expired for the auth login.", 50 | short: "not_found", 51 | }; 52 | } 53 | 54 | return { ok: true, content: command[0].keycode }; 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /src/database/authStoreStateKey.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function authStoreStateKey 4 | * @desc Gets a state key from login process and saves it on the database. 5 | * @param {string} stateKey - The key code string. 6 | * @returns {object} A server status object. 7 | */ 8 | 9 | module.exports = { 10 | safe: false, 11 | exec: async (sql, stateKey) => { 12 | const command = await sql` 13 | INSERT INTO authstate (keycode) 14 | VALUES (${stateKey}) 15 | RETURNING keycode; 16 | `; 17 | 18 | return command.count !== 0 19 | ? { ok: true, content: command[0].keycode } 20 | : { 21 | ok: false, 22 | content: `The state key has not been saved on the database.`, 23 | short: "server_error", 24 | }; 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/database/getFeaturedPackages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function getFeaturedPackages 4 | * @desc Collects the hardcoded featured packages array from the storage.js 5 | * module. Then uses this.getPackageCollectionByName to retrieve details of the 6 | * package. 7 | * @returns {object} A server status object. 8 | */ 9 | 10 | const storage = require("../storage.js"); 11 | const getPackageCollectionByName = 12 | require("./getPackageCollectionByName.js").exec; 13 | 14 | module.exports = { 15 | safe: false, 16 | exec: async (sql) => { 17 | let featuredArray = await storage.getFeaturedPackages(); 18 | 19 | if (!featuredArray.ok) { 20 | return featuredArray; 21 | } 22 | 23 | return await getPackageCollectionByName(sql, featuredArray.content); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/database/getFeaturedThemes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function getFeaturedThemes 4 | * @desc Collects the hardcoded featured themes array from the storage.js module. 5 | * Then uses this.getPackageCollectionByName to retrieve details of the package. 6 | * @returns {object} A server status object. 7 | */ 8 | const storage = require("../storage.js"); 9 | const getPackageCollectionByName = 10 | require("./getPackageCollectionByName.js").exec; 11 | 12 | module.exports = { 13 | safe: false, 14 | exec: async (sql) => { 15 | let featuredThemeArray = await storage.getFeaturedThemes(); 16 | 17 | if (!featuredThemeArray.ok) { 18 | return featuredThemeArray; 19 | } 20 | 21 | return await getPackageCollectionByName(sql, featuredThemeArray.content); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/database/getPackageByName.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function getPackageByName 4 | * @desc Takes a package name and returns the raw SQL package with all its versions. 5 | * This module is also used to get the data to be sent to utils.constructPackageObjectFull() 6 | * in order to convert the query result in Package Object Full format. 7 | * In that case it's recommended to set the user flag as true for security reasons. 8 | * @param {string} name - The name of the package. 9 | * @param {bool} user - Whether the packages has to be exposed outside or not. 10 | * If true, all sensitive data like primary and foreign keys are not selected. 11 | * Even if the keys are ignored by utils.constructPackageObjectFull(), it's still 12 | * safe to not inclue them in case, by mistake, we publish the return of this module. 13 | * @returns {object} A server status object. 14 | */ 15 | 16 | module.exports = { 17 | safe: false, 18 | exec: async (sql, name, user = false) => { 19 | const command = await sql` 20 | SELECT 21 | ${ 22 | user ? sql`` : sql`p.pointer,` 23 | } p.name, p.created, p.updated, p.creation_method, p.downloads, p.data, p.owner, 24 | (p.stargazers_count + p.original_stargazers) AS stargazers_count, 25 | JSONB_AGG( 26 | JSON_BUILD_OBJECT( 27 | ${ 28 | user ? sql`` : sql`'id', v.id, 'package', v.package,` 29 | } 'semver', v.semver, 'license', v.license, 'engine', v.engine, 'meta', v.meta, 30 | 'hasGrammar', v.has_grammar, 'hasSnippets', v.has_snippets, 31 | 'supportedLanguages', v.supported_languages 32 | ) 33 | ORDER BY v.semver_v1 DESC, v.semver_v2 DESC, v.semver_v3 DESC, v.created DESC 34 | ) AS versions 35 | FROM packages AS p 36 | INNER JOIN names AS n ON (p.pointer = n.pointer AND n.name = ${name}) 37 | INNER JOIN versions AS v ON (p.pointer = v.package AND v.deleted IS FALSE) 38 | GROUP BY p.pointer; 39 | `; 40 | 41 | return command.count !== 0 42 | ? { ok: true, content: command[0] } 43 | : { 44 | ok: false, 45 | content: `package ${name} not found.`, 46 | short: "not_found", 47 | }; 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/database/getPackageByNameSimple.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function getPackageByNameSimple 4 | * @desc Internal util used by other functions in this module to get the package row by the given name. 5 | * It's like getPackageByName(), but with a simple and faster query. 6 | * @param {string} name - The name of the package. 7 | * @returns {object} A server status object. 8 | */ 9 | 10 | module.exports = { 11 | safe: false, 12 | exec: async (sql, name) => { 13 | const command = await sql` 14 | SELECT pointer FROM names 15 | WHERE name = ${name}; 16 | `; 17 | 18 | return command.count !== 0 19 | ? { ok: true, content: command[0] } 20 | : { 21 | ok: false, 22 | content: `Package ${name} not found.`, 23 | short: "not_found", 24 | }; 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/database/getPackageCollectionByID.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function getPackageCollectionByID 4 | * @desc Takes a package pointer array, and returns an array of the package objects. 5 | * @param {int[]} packArray - An array of package id. 6 | * @returns {object} A server status object. 7 | */ 8 | 9 | module.exports = { 10 | safe: false, 11 | exec: async (sql, packArray) => { 12 | const command = await sql` 13 | SELECT DISTINCT ON (p.name) p.name, v.semver, p.downloads, 14 | (p.stargazers_count + p.original_stargazers) AS stargazers_count, p.data 15 | FROM packages AS p 16 | INNER JOIN versions AS v ON (p.pointer = v.package AND v.deleted IS FALSE) 17 | WHERE pointer IN ${sql(packArray)} 18 | ORDER BY p.name, v.semver_v1 DESC, v.semver_v2 DESC, v.semver_v3 DESC, v.created DESC; 19 | `; 20 | 21 | return command.count !== 0 22 | ? { ok: true, content: command } 23 | : { ok: false, content: "No packages found.", short: "Not Found" }; 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/database/getPackageCollectionByName.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function getPackageCollectionByName 4 | * @desc Takes a package name array, and returns an array of the package objects. 5 | * You must ensure that the packArray passed is compatible. This function does not coerce compatibility. 6 | * @param {string[]} packArray - An array of package name strings. 7 | * @returns {object} A server status object. 8 | */ 9 | 10 | module.exports = { 11 | safe: false, 12 | exec: async (sql, packArray) => { 13 | // Since this function is invoked by getFeaturedThemes and getFeaturedPackages 14 | // which process the returned content with constructPackageObjectShort(), 15 | // we select only the needed columns. 16 | const command = await sql` 17 | SELECT DISTINCT ON (p.name) p.name, v.semver, p.downloads, p.owner, 18 | (p.stargazers_count + p.original_stargazers) AS stargazers_count, p.data 19 | FROM packages AS p 20 | INNER JOIN names AS n ON (p.pointer = n.pointer AND n.name IN ${sql( 21 | packArray 22 | )}) 23 | INNER JOIN versions AS v ON (p.pointer = v.package AND v.deleted IS FALSE) 24 | ORDER BY p.name, v.semver_v1 DESC, v.semver_v2 DESC, v.semver_v3 DESC, v.created DESC; 25 | `; 26 | 27 | return command.count !== 0 28 | ? { ok: true, content: command } 29 | : { ok: false, content: "No packages found.", short: "not_found" }; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/database/getPackageVersionByNameAndVersion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function getPackageVersionByNameAndVersion 4 | * @desc Uses the name of a package and it's version to return the version info. 5 | * @param {string} name - The name of the package to query. 6 | * @param {string} version - The version of the package to query. 7 | * @returns {object} A server status object. 8 | */ 9 | 10 | module.exports = { 11 | safe: false, 12 | exec: async (sql, name, version) => { 13 | const command = await sql` 14 | SELECT v.semver, v.license, v.engine, v.meta 15 | FROM packages AS p 16 | INNER JOIN names AS n ON (p.pointer = n.pointer AND n.name = ${name}) 17 | INNER JOIN versions AS v ON (p.pointer = v.package AND v.semver = ${version} AND v.deleted IS FALSE); 18 | `; 19 | 20 | return command.count !== 0 21 | ? { ok: true, content: command[0] } 22 | : { 23 | ok: false, 24 | content: `Package ${name} and Version ${version} not found.`, 25 | short: "not_found", 26 | }; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/database/getStarredPointersByUserID.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function getStarredPointersByUserID 4 | * @description Get all packages which the user gave the star. 5 | * The result of this function should not be returned to the user because it contains pointers UUID. 6 | * @param {int} userid - ID of the user. 7 | * @returns {object} A server status object. 8 | */ 9 | 10 | module.exports = { 11 | safe: false, 12 | exec: async (sql, userid) => { 13 | const command = await sql` 14 | SELECT ARRAY ( 15 | SELECT package FROM stars WHERE userid = ${userid} 16 | ); 17 | `; 18 | 19 | // It is likely safe to assume that if nothing matches the userid, 20 | // then the user hasn't given any star. So instead of server error 21 | // here we will non-traditionally return an empty array. 22 | const packArray = 23 | command.count !== 0 && Array.isArray(command[0].array) 24 | ? command[0].array 25 | : []; 26 | 27 | return { ok: true, content: packArray }; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/database/getStarringUsersByPointer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function getStarringUsersByPointer 4 | * @description Use the pointer of a package to collect all users that have starred it. 5 | * @param {string} pointer - The ID of the package. 6 | * @returns {object} A server status object. 7 | */ 8 | 9 | const logger = require("../logger.js"); 10 | 11 | module.exports = { 12 | safe: false, 13 | exec: async (sql, pointer) => { 14 | const command = await sql` 15 | SELECT ARRAY ( 16 | SELECT userid FROM stars WHERE package = ${pointer.pointer} 17 | ); 18 | `; 19 | 20 | let userArray = command[0].array; 21 | 22 | if (command.count === 0) { 23 | // It is likely safe to assume that if nothing matches the packagepointer, 24 | // then the package pointer has no stars. So instead of server error 25 | // here we will non-traditionally return an empty array. 26 | logger.generic( 27 | 3, 28 | `No Stars for ${pointer} found, assuming 0 star value.` 29 | ); 30 | userArray = []; 31 | } 32 | 33 | return { ok: true, content: userArray }; 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/database/getUserByID.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function getUserByID 4 | * @desc Get user details providing their ID. 5 | * @param {int} id - User ID 6 | * @returns {object} A Server status Object. 7 | */ 8 | 9 | module.exports = { 10 | safe: false, 11 | exec: async (sql, id) => { 12 | const command = await sql` 13 | SELECT * FROM users 14 | WHERE id = ${id}; 15 | `; 16 | 17 | if (command.count === 0) { 18 | return { 19 | ok: false, 20 | content: `Unable to get user by ID: ${id}`, 21 | short: "server_error", 22 | }; 23 | } 24 | 25 | return command.count !== 0 26 | ? { ok: true, content: command[0] } 27 | : { 28 | ok: false, 29 | content: `Unable to get user by ID: ${id}`, 30 | short: "server_error", 31 | }; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/database/getUserByName.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function getUserByName 4 | * @description Get a users details providing their username. 5 | * @param {string} username - User name string. 6 | * @returns {object} A server status object. 7 | */ 8 | 9 | module.exports = { 10 | safe: false, 11 | exec: async (sql, username) => { 12 | const command = await sql` 13 | SELECT * FROM users 14 | WHERE username = ${username}; 15 | `; 16 | 17 | return command.count !== 0 18 | ? { ok: true, content: command[0] } 19 | : { 20 | ok: false, 21 | content: `Unable to query for user: ${username}`, 22 | short: "not_found", 23 | }; 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/database/getUserByNodeID.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function getUserByNodeID 4 | * @description Get user details providing their Node ID. 5 | * @param {string} id - Users Node ID. 6 | * @returns {object} A server status object. 7 | */ 8 | 9 | module.exports = { 10 | safe: false, 11 | exec: async (sql, id) => { 12 | const command = await sql` 13 | SELECT * FROM users 14 | WHERE node_id = ${id}; 15 | `; 16 | 17 | if (command.count === 0) { 18 | return { 19 | ok: false, 20 | content: `Unable to get User By NODE_ID: ${id}`, 21 | short: "not_found", 22 | }; 23 | } 24 | 25 | return command.count !== 0 26 | ? { ok: true, content: command[0] } 27 | : { 28 | ok: false, 29 | content: `Unable to get User By NODE_ID: ${id}`, 30 | short: "server_error", 31 | }; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/database/getUserCollectionById.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function getUserCollectionById 4 | * @description Returns an array of Users and their associated data via the ids. 5 | * @param {array} ids - The IDs of users to collect the data of. 6 | * @returns {object} A server status object with the array of users collected. 7 | */ 8 | 9 | const getUserByID = require("./getUserByID.js").exec; 10 | const logger = require("../logger.js"); 11 | 12 | module.exports = { 13 | safe: false, 14 | exec: async (sql, ids) => { 15 | let userArray = []; 16 | 17 | for (let i = 0; i < ids.length; i++) { 18 | let user = await getUserByID(sql, ids[i]); 19 | 20 | if (!user.ok) { 21 | logger.generic(3, "Unable to find user id: ", { 22 | type: "object", 23 | obj: ids[i], 24 | }); 25 | logger.generic(3, "Details on Not Found User: ", { 26 | type: "object", 27 | obj: user, 28 | }); 29 | continue; 30 | } 31 | 32 | userArray.push({ login: user.content.username }); 33 | } 34 | 35 | return { ok: true, content: userArray }; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/database/insertNewUser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function insertNewUser 4 | * @desc Insert a new user into the database. 5 | * @param {string} username - Username of the user. 6 | * @param {object} id - Identifier code of the user. 7 | * @param {object} avatar - The avatar of the user. 8 | * @returns {object} A server status object. 9 | */ 10 | 11 | module.exports = { 12 | safe: false, 13 | exec: async (sql, username, id, avatar) => { 14 | const command = await sql` 15 | INSERT INTO users (username, node_id, avatar) 16 | VALUES (${username}, ${id}, ${avatar}) 17 | RETURNING *; 18 | `; 19 | 20 | return command.count !== 0 21 | ? { ok: true, content: command[0] } 22 | : { 23 | ok: false, 24 | content: `Unable to create user: ${username}`, 25 | short: "Server Error", 26 | }; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/database/packageNameAvailability.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function packageNameAvailability 4 | * @desc Determines if a name is ready to be used for a new package. Useful in the stage of the publication 5 | * of a new package where checking if the package exists is not enough because a name could be not 6 | * available if a deleted package was using it in the past. 7 | * Useful also to check if a name is available for the renaming of a published package. 8 | * This function simply checks if the provided name is present in "names" table. 9 | * @param {string} name - The candidate name for a new package. 10 | * @returns {object} A Server Status Object. 11 | */ 12 | 13 | module.exports = { 14 | safe: true, 15 | exec: async (sql, name) => { 16 | const command = await sql` 17 | SELECT name FROM names 18 | WHERE name = ${name}; 19 | `; 20 | 21 | return command.count === 0 22 | ? { 23 | ok: true, 24 | content: `${name} is available to be used for a new package.`, 25 | } 26 | : { 27 | ok: false, 28 | content: `${name} is not available to be used for a new package.`, 29 | short: "not_found", 30 | }; 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/database/updateDecrementStar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function updateDecrementStar 4 | * @description Register the removal of the star on a package by a user. 5 | * @param {int} user - User Object who remove the star. 6 | * @param {string} pack - Package name that get the star removed. 7 | * @returns {object} A server status object. 8 | */ 9 | 10 | const getPackageByNameSimple = require("./getPackageByNameSimple.js").exec; 11 | const updatePackageStargazers = require("./updatePackageStargazers.js").exec; 12 | 13 | module.exports = { 14 | safe: false, 15 | exec: async (sql, user, pack) => { 16 | const packID = await getPackageByNameSimple(sql, pack); 17 | 18 | if (!packID.ok) { 19 | return { 20 | ok: false, 21 | content: `Unable to find package ${pack} to unstar.`, 22 | short: "not_found", 23 | }; 24 | } 25 | 26 | const pointer = packID.content.pointer; 27 | 28 | const commandUnstar = await sql` 29 | DELETE FROM stars 30 | WHERE (package = ${pointer}) AND (userid = ${user.id}) 31 | RETURNING *; 32 | `; 33 | 34 | if (commandUnstar.count === 0) { 35 | // We know user and package exist both, so the fail is because 36 | // the star was already missing, 37 | // The user expects its star is not given, so we return ok. 38 | return { 39 | ok: true, 40 | content: "The Star is Already Missing", 41 | }; 42 | } 43 | 44 | // If the return does not match our input, it failed. 45 | if ( 46 | user.id !== commandUnstar[0].userid || 47 | pointer !== commandUnstar[0].package 48 | ) { 49 | return { 50 | ok: false, 51 | content: "Failed to Unstar the Package", 52 | short: "server_error", 53 | }; 54 | } 55 | 56 | // Now update the stargazers count into the packages table 57 | const updatePack = await updatePackageStargazers(sql, pack, pointer); 58 | 59 | if (!updatePack.ok) { 60 | return updatePack; 61 | } 62 | 63 | return { 64 | ok: true, 65 | content: "Package Successfully Unstarred", 66 | }; 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /src/database/updateIncrementStar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function updateIncrementStar 4 | * @description Register the star given by a user to a package. 5 | * @param {int} user - A User Object that should star the package. 6 | * @param {string} pack - Package name that get the new star. 7 | * @returns {object} A server status object. 8 | */ 9 | 10 | const getPackageByNameSimple = require("./getPackageByNameSimple.js").exec; 11 | const updatePackageStargazers = require("./updatePackageStargazers.js").exec; 12 | 13 | module.exports = { 14 | safe: false, 15 | exec: async (sql, user, pack) => { 16 | const packID = await getPackageByNameSimple(sql, pack); 17 | 18 | if (!packID.ok) { 19 | return { 20 | ok: false, 21 | content: `Unable to find package ${pack} to star.`, 22 | short: "not_found", 23 | }; 24 | } 25 | 26 | const pointer = packID.content.pointer; 27 | 28 | try { 29 | const commandStar = await sql` 30 | INSERT INTO stars 31 | (package, userid) VALUES 32 | (${pointer}, ${user.id}) 33 | RETURNING *; 34 | `; 35 | 36 | // Now we expect to get our data right back, and can check the 37 | // validity to know if this happened successfully or not. 38 | if ( 39 | pointer !== commandStar[0].package || 40 | user.id !== commandStar[0].userid 41 | ) { 42 | return { 43 | ok: false, 44 | content: `Failed to Star the Package`, 45 | short: "server_error", 46 | }; 47 | } 48 | 49 | // Now update the stargazers count into the packages table 50 | const updatePack = await updatePackageStargazers(sql, pack, pointer); 51 | 52 | if (!updatePack.ok) { 53 | return updatePack; 54 | } 55 | 56 | return { 57 | ok: true, 58 | content: `Package Successfully Starred`, 59 | }; 60 | } catch (e) { 61 | // TODO: While the comment below is accurate 62 | // It's also worth noting that this catch will return success 63 | // If the starring user does not exist. Resulting in a false positive 64 | // Catch the primary key violation on (package, userid), 65 | // Sinche the package is already starred by the user, we return ok. 66 | return { 67 | ok: true, 68 | content: `Package Already Starred`, 69 | }; 70 | } 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /src/database/updatePackageDecrementDownloadByName.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function updatePackageDecrementDownloadByName 4 | * @description Uses the package name to decrement the download count by one. 5 | * @param {string} name - The package name. 6 | * @returns {object} The modified server status object. 7 | */ 8 | 9 | module.exports = { 10 | safe: false, 11 | exec: async (sql, name) => { 12 | const command = await sql` 13 | UPDATE packages AS p 14 | SET downloads = GREATEST(p.downloads - 1, 0) 15 | FROM names AS n 16 | WHERE n.pointer = p.pointer AND n.name = ${name} 17 | RETURNING p.name, p.downloads; 18 | `; 19 | 20 | return command.count !== 0 21 | ? { ok: true, content: command[0] } 22 | : { 23 | ok: false, 24 | content: "Unable to decrement Package Download Count", 25 | short: "server_error", 26 | }; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/database/updatePackageIncrementDownloadByName.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function updatePackageIncrementDownloadByName 4 | * @description Uses the package name to increment the download count by one. 5 | * @param {string} name - The package name. 6 | * @returns {object} The modified server status object. 7 | */ 8 | 9 | module.exports = { 10 | safe: false, 11 | exec: async (sql, name) => { 12 | const command = await sql` 13 | UPDATE packages AS p 14 | SET downloads = p.downloads + 1 15 | FROM names AS n 16 | WHERE n.pointer = p.pointer AND n.name = ${name} 17 | RETURNING p.name, p.downloads; 18 | `; 19 | 20 | return command.count !== 0 21 | ? { ok: true, content: command[0] } 22 | : { 23 | ok: false, 24 | content: "Unable to Update Package Download", 25 | short: "Server Error", 26 | }; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/database/updatePackageStargazers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function updatePackageStargazers 4 | * @description Internal util that uses the package name (or pointer if provided) to update its stargazers count. 5 | * @param {string} name - The package name. 6 | * @param {string} pointer - The package id (if given, the search by name is skipped). 7 | * @returns {object} The effected server status object. 8 | */ 9 | 10 | const getPackageByNameSimple = require("./getPackageByNameSimple.js"); 11 | 12 | module.exports = { 13 | safe: false, 14 | exec: async (sql, name, pointer = null) => { 15 | if (pointer === null) { 16 | const packID = await getPackageByNameSimple(sql, name); 17 | 18 | if (!packID.ok) { 19 | return packID; 20 | } 21 | 22 | pointer = packID.content.pointer; 23 | } 24 | 25 | const countStars = await sql` 26 | SELECT COUNT(*) AS stars 27 | FROM stars 28 | WHERE package = ${pointer}; 29 | `; 30 | 31 | const starCount = countStars.count !== 0 ? countStars[0].stars : 0; 32 | 33 | const updateStar = await sql` 34 | UPDATE packages 35 | SET stargazers_count = ${starCount} 36 | WHERE pointer = ${pointer} 37 | RETURNING name, (stargazers_count + original_stargazers) AS stargazers_count; 38 | `; 39 | 40 | return updateStar.count !== 0 41 | ? { ok: true, content: updateStar[0] } 42 | : { 43 | ok: false, 44 | content: "Unable to Update Package Stargazers", 45 | short: "server_error", 46 | }; 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/models/constructPackageObjectJSON.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @async 3 | * @function constructPackageObjectJSON 4 | * @desc Takes the return of getPackageVersionByNameAndVersion and returns 5 | * a recreation of the package.json with a modified dist.tarball key, pointing 6 | * to this server for download. 7 | * @param {object} pack - The expected raw SQL return of `getPackageVersionByNameAndVersion` 8 | * @returns {object} A properly formatted Package Object Mini. 9 | * @see {@link https://github.com/confused-Techie/atom-backend/blob/main/docs/returns.md#package-object-mini} 10 | */ 11 | 12 | const logger = require("../logger.js"); 13 | const { server_url } = require("../config.js").getConfig(); 14 | 15 | module.exports = async function constructPackageObjectJSON(pack) { 16 | const parseVersionObject = (v) => { 17 | let newPack = v.meta; 18 | if (newPack.sha) { 19 | delete newPack.sha; 20 | } 21 | if (newPack.tarball_url) { 22 | delete newPack.tarball_url; 23 | } 24 | newPack.dist ??= {}; 25 | newPack.dist.tarball = `${server_url}/api/packages/${v.meta.name}/versions/${v.semver}/tarball`; 26 | newPack.engines = v.engines; 27 | logger.generic(6, "Single Package Object JSON finished without Error"); 28 | return newPack; 29 | }; 30 | 31 | if (!Array.isArray(pack)) { 32 | const newPack = parseVersionObject(pack); 33 | 34 | logger.generic(6, "Single Package Object JSON finished without Error"); 35 | return newPack; 36 | } 37 | 38 | let arrPack = []; 39 | for (const p of pack) { 40 | arrPack.push(parseVersionObject(p)); 41 | } 42 | 43 | logger.generic(66, "Array Package Object JSON finished without Error"); 44 | return arrPack; 45 | }; 46 | -------------------------------------------------------------------------------- /src/models/ssoHTML.js: -------------------------------------------------------------------------------- 1 | const SSO = require("./sso.js"); 2 | 3 | module.exports = class SSOHTML extends SSO { 4 | constructor() { 5 | super(); 6 | } 7 | 8 | handleSuccess(req, res, context) { 9 | res.send(this.content); 10 | context.logger.httpLog(req, res); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/models/ssoPaginate.js: -------------------------------------------------------------------------------- 1 | const SSO = require("./sso.js"); 2 | 3 | module.exports = class SSOPaginate extends SSO { 4 | constructor() { 5 | super(); 6 | 7 | this.link = ""; 8 | this.totalPages = 0; 9 | this.resultCount = 0; 10 | this.limit = 0; 11 | } 12 | 13 | buildLink(url, currentPage, params) { 14 | let paramString = ""; 15 | 16 | for (let param in params) { 17 | // We manually assign the page query so we will skip 18 | if (param === "page") { 19 | continue; 20 | } 21 | if (param === "query") { 22 | // Since we know we want to keep search queries safe strings 23 | const safeQuery = encodeURIComponent( 24 | params[param].replace(/[<>"':;\\/]+/g, "") 25 | ); 26 | paramString += `&${param}=${safeQuery}`; 27 | } else { 28 | paramString += `&${param}=${params[param]}`; 29 | } 30 | } 31 | 32 | let linkString = ""; 33 | 34 | linkString += `<${url}?page=${currentPage}${paramString}>; rel="self", `; 35 | linkString += `<${url}?page=${this.totalPages}${paramString}>; rel="last"`; 36 | 37 | if (currentPage !== this.totalPages) { 38 | linkString += `, <${url}?page=${ 39 | parseInt(currentPage) + 1 40 | }${paramString}>; rel="next"`; 41 | } 42 | 43 | this.link = linkString; 44 | } 45 | 46 | handleSuccess(req, res, context) { 47 | res.append("Link", this.link); 48 | res.append("Query-Total", this.resultCount); 49 | res.append("Query-Limit", this.limit); 50 | 51 | res.status(this.successStatusCode).json(this.content); 52 | context.logger.httpLog(req, res); 53 | return; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/models/ssoRedirect.js: -------------------------------------------------------------------------------- 1 | const SSO = require("./sso.js"); 2 | 3 | module.exports = class SSORedirect extends SSO { 4 | constructor() { 5 | super(); 6 | } 7 | 8 | handleSuccess(req, res, context) { 9 | res.redirect(this.content); 10 | context.logger.httpLog(req, res); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/query_parameters/auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function auth 3 | * @desc Retrieves Authorization Headers from Request, and Checks for Undefined. 4 | * @param {object} req - The `Request` object inherited from the Express endpoint. 5 | * @returns {string} Returning a valid Authorization Token, or '' if invalid/not found. 6 | */ 7 | module.exports = { 8 | schema: { 9 | name: "auth", 10 | in: "header", 11 | schema: { 12 | type: "string", 13 | }, 14 | required: true, 15 | allowEmptyValue: false, 16 | description: "Authorization Headers.", 17 | }, 18 | logic: (req) => { 19 | const token = req.get("Authorization"); 20 | 21 | return token ?? ""; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/query_parameters/direction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function direction 3 | * @desc Parser for either 'direction' or 'order' query parameter, prioritizing 4 | * 'direction'. 5 | * @param {object} req - The `Request` object inherited from the Express endpoint. 6 | * @returns {string} The valid direction value from the 'direction' or 'order' 7 | * query parameter. 8 | */ 9 | module.exports = { 10 | schema: { 11 | name: "direction", 12 | in: "query", 13 | schema: { 14 | type: "string", 15 | enum: ["desc", "asc"], 16 | default: "desc", 17 | }, 18 | example: "desc", 19 | allowEmptyValue: true, 20 | description: 21 | "Direction to list search results. Also accepts 'order' for backwards compatibility.", 22 | }, 23 | logic: (req) => { 24 | const def = "desc"; 25 | const valid = ["asc", "desc"]; 26 | 27 | // Seems that the autolink headers use order, while documentation uses direction. 28 | // Since we are not sure where in the codebase it uses the other, we will just accept both. 29 | const prov = req.query.direction ?? req.query.order ?? def; 30 | 31 | return valid.includes(prov) ? prov : def; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/query_parameters/engine.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function engine 3 | * @desc Parses the 'engine' query parameter to ensure it's valid, otherwise returning false. 4 | * @param {string} semver - The engine string. 5 | * @returns {string|boolean} Returns the valid 'engine' specified, or if none, returns false. 6 | */ 7 | module.exports = { 8 | schema: { 9 | name: "engine", 10 | in: "query", 11 | schema: { 12 | type: "string", 13 | }, 14 | example: "1.0.0", 15 | allowEmptyValue: true, 16 | description: 17 | "Only show packages compatible with this Pulsar version. Must be a valid Semver.", 18 | }, 19 | // TODO: Why does this accept `semver` and not a request object? 20 | logic: (semver) => { 21 | try { 22 | // Regex inspired by: 23 | // - https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string 24 | // - https://regex101.com/r/vkijKf/1/ 25 | // The only difference is that we truncate the check for additional labels because we want to be 26 | // as permissive as possible and need only the first three version numbers. 27 | 28 | const regex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)/; 29 | 30 | // Check if it's a valid semver 31 | return semver.match(regex) !== null ? semver : false; 32 | } catch (e) { 33 | return false; 34 | } 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/query_parameters/fileExtension.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function fileExtension 3 | * @desc Returns the file extension being requested. 4 | * @param {object} req - The `Request` object inherited from the Express endpoint. 5 | * @returns {string|boolean} Returns false if the provided value is invalid, or 6 | * nonexistant. Returns the service string otherwise. 7 | */ 8 | const utils = require("./utils.js"); 9 | 10 | module.exports = { 11 | schema: { 12 | name: "fileExtension", 13 | in: "query", 14 | schema: { 15 | type: "string", 16 | }, 17 | example: "coffee", 18 | allowEmptyValue: true, 19 | description: 20 | "File extension for which to show only compatible grammar packages of.", 21 | }, 22 | logic: (req) => { 23 | return utils.stringValidation(req.query.fileExtension); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/query_parameters/filter.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | schema: { 3 | name: "filter", 4 | in: "query", 5 | schema: { 6 | type: "string", 7 | enum: ["package", "theme"], 8 | default: "package", 9 | }, 10 | required: false, 11 | allowEmptyValue: false, 12 | example: "package", 13 | description: 14 | "Deprecated method to display packages or themes. Use `/api/themes/search` or `/api/packages/search` instead.", 15 | }, 16 | logic: (req) => { 17 | const def = "package"; 18 | const valid = ["theme", "package"]; 19 | 20 | const prov = req.query.filter; 21 | 22 | if (typeof prov !== "string") { 23 | return def; 24 | } 25 | 26 | if (!valid.includes(prov)) { 27 | return def; 28 | } 29 | 30 | return prov; 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/query_parameters/login.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function login 3 | * @desc Returns the User from the URL Path, otherwise '' 4 | * @param {object} req - The `Request` object inherited from the Express endpoint. 5 | * @returns {string} Returns a valid specified user or ''. 6 | */ 7 | 8 | module.exports = { 9 | schema: { 10 | name: "login", 11 | in: "path", 12 | schema: { 13 | type: "string", 14 | }, 15 | required: true, 16 | allowEmptyValue: false, 17 | example: "confused-Techie", 18 | description: "The User from the URL Path.", 19 | }, 20 | logic: (req) => { 21 | return req.params.login ?? ""; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/query_parameters/owner.js: -------------------------------------------------------------------------------- 1 | const ownerShared = require("./shared/owner.js"); 2 | 3 | module.exports = { 4 | schema: { 5 | name: "owner", 6 | in: "query", 7 | schema: { 8 | type: "string", 9 | }, 10 | example: "pulsar-edit", 11 | allowEmptyValue: false, 12 | required: false, 13 | description: "Owner to filter results by.", 14 | }, 15 | logic: (req) => { 16 | let prov = req.query.owner ?? null; 17 | 18 | return ownerShared(prov); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/query_parameters/ownerName.js: -------------------------------------------------------------------------------- 1 | const ownerShared = require("./shared/owner.js"); 2 | 3 | module.exports = { 4 | schema: { 5 | name: "ownerName", 6 | in: "path", 7 | schema: { 8 | type: "string", 9 | }, 10 | example: "pulsar-edit", 11 | allowEmptyValue: false, 12 | required: true, 13 | description: "Owner of packages to retrieve.", 14 | }, 15 | logic: (req) => { 16 | let prov = req.params.ownerName ?? null; 17 | 18 | return ownerShared(prov); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/query_parameters/packageName.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function packageName 3 | * @desc This function will convert a user provided package name into a safe format. 4 | * It ensures the name is converted to lower case. As is the requirement of all package names. 5 | * @param {object} req - The `Request` Object inherited from the Express endpoint. 6 | * @returns {string} Returns the package name in a safe format that can be worked with further. 7 | */ 8 | module.exports = { 9 | schema: { 10 | name: "packageName", 11 | in: "path", 12 | schema: { 13 | type: "string", 14 | }, 15 | required: true, 16 | allowEmptyValue: false, 17 | example: "autocomplete-powershell", 18 | description: 19 | "The name of the package to return details for. Must be URL escaped.", 20 | }, 21 | logic: (req) => { 22 | return req.params.packageName.toLowerCase(); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/query_parameters/page.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function page 3 | * @desc Parser of the Page query parameter. Defaulting to 1. 4 | * @param {object} req - The `Request` object inherited from the Express endpoint. 5 | * @returns {number} Returns the valid page provided in the query parameter or 1, as the default. 6 | */ 7 | 8 | module.exports = { 9 | schema: { 10 | name: "page", 11 | in: "query", 12 | schema: { 13 | type: "number", 14 | minimum: 1, 15 | default: 1, 16 | }, 17 | example: 1, 18 | allowEmptyValue: true, 19 | required: false, 20 | description: "The page of available results to return.", 21 | }, 22 | logic: (req) => { 23 | const def = 1; 24 | const prov = req.query.page; 25 | 26 | switch (typeof prov) { 27 | case "string": { 28 | const n = parseInt(prov, 10); 29 | return isNaN(prov) ? def : n; 30 | } 31 | 32 | case "number": 33 | return isNaN(prov) ? def : prov; 34 | 35 | default: 36 | return def; 37 | } 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /src/query_parameters/query.js: -------------------------------------------------------------------------------- 1 | const utils = require("./utils.js"); 2 | /** 3 | * @function query 4 | * @desc Checks the 'q' query parameter, trunicating it at 50 characters, and checking simplisticly that 5 | * it is not a malicious request. Returning "" if an unsafe or invalid query is passed. 6 | * @param {object} req - The `Request` object inherited from the Express endpoint. 7 | * @returns {string} A valid search string derived from 'q' query parameter. Or '' if invalid. 8 | * @implements {pathTraversalAttempt} 9 | */ 10 | 11 | module.exports = { 12 | schema: { 13 | name: "q", 14 | in: "query", 15 | schema: { 16 | type: "string", 17 | }, 18 | example: "generic-lsp", 19 | required: true, 20 | description: "Search Query", 21 | }, 22 | logic: (req) => { 23 | const maxLength = 50; // While package.json names according to NPM can be up to 214 characters, 24 | // for performance on the server and assumed deminishing returns on longer queries, 25 | // this is cut off at 50 as suggested by Digitalone1. 26 | const prov = req.query.q; 27 | 28 | if (typeof prov !== "string") { 29 | return ""; 30 | } 31 | 32 | // If there is a path traversal attach detected return empty query. 33 | // Additionally do not allow strings longer than `maxLength` 34 | return utils.pathTraversalAttempt(prov) 35 | ? "" 36 | : prov.slice(0, maxLength).trim(); 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/query_parameters/rename.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function rename 3 | * @desc Since this is intended to be returning a boolean value, returns false 4 | * if invalid, otherwise returns true. Checking for mixed captilization. 5 | * @param {object} req - The `Request` object inherited from the Express endpoint. 6 | * @returns {boolean} Returns false if invalid, or otherwise returns the boolean value of the string. 7 | */ 8 | 9 | module.exports = { 10 | schema: { 11 | name: "rename", 12 | in: "query", 13 | schema: { 14 | type: "string", 15 | }, 16 | example: "new-package-name", 17 | allowEmptyValue: false, 18 | required: false, 19 | description: "The new package name to rename to, if applicable.", 20 | }, 21 | logic: (req) => { 22 | const prov = req.query.rename; 23 | 24 | if (prov === undefined) { 25 | // Originally it was believed that this query parameter should be handled as 26 | // if it was a text passed boolean. But appears to actually provide the string 27 | // of text the package should be renamed too. 28 | return false; 29 | } 30 | 31 | // Due to the backend already being built in such a way that it will rename 32 | // a package by finding the rename value on it's own, we will still return a 33 | // boolean, but TODO:: this should be fixed in the future. 34 | 35 | if (typeof prov === "string" && prov.length > 0) { 36 | return true; 37 | } 38 | 39 | return false; 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/query_parameters/repository.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function repo 3 | * @desc Parses the 'repository' query parameter, returning it if valid, otherwise returning ''. 4 | * @param {object} req - The `Request` object inherited from the Express endpoint. 5 | * @returns {string} Returning the valid 'repository' query parameter, or false if invalid. 6 | */ 7 | const parseGithubURL = require("parse-github-url"); 8 | 9 | module.exports = { 10 | schema: { 11 | name: "repository", 12 | in: "query", 13 | schema: { 14 | type: "string", 15 | }, 16 | example: "pulsar-edit/pulsar", 17 | allowEmptyValue: false, 18 | required: true, 19 | description: "Repository to publish.", 20 | }, 21 | logic: (req) => { 22 | const prov = req.query.repository; 23 | 24 | if (prov === undefined) { 25 | return false; 26 | } 27 | 28 | const parsed = parseGithubURL(prov); 29 | 30 | if (typeof parsed.owner !== "string" || typeof parsed.name !== "string") { 31 | return false; 32 | } 33 | 34 | const re = /^[^._ ][^ ]{0,213}$/; 35 | // Ensure both the name and owner: 36 | // - less than or equal to 214 characters 37 | // - cannot begin with a dot or an underscore 38 | // - cannot contain a space 39 | 40 | if (parsed.owner.match(re) === null || parsed.name.match(re) === null) { 41 | return false; 42 | } 43 | 44 | return prov; 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/query_parameters/service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function service 3 | * @desc Returns the service being requested. 4 | * @param {object} req - The `Request` object inherited from the Express endpoint. 5 | * @returns {string|boolean} Returns false if the provided value is invalid or 6 | * nonexistent. Returns the service string otherwise. 7 | */ 8 | const utils = require("./utils.js"); 9 | 10 | module.exports = { 11 | schema: { 12 | name: "service", 13 | in: "query", 14 | schema: { 15 | type: "string", 16 | }, 17 | example: "autocomplete.watchEditor", 18 | allowEmptyValue: true, 19 | description: "The service of which to filter packages by.", 20 | }, 21 | logic: (req) => { 22 | return utils.stringValidation(req.query.service); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/query_parameters/serviceType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function serviceType 3 | * @desc Returns the service type being requested. 4 | * @param {object} req - The `Request` object inherited from the Express endpoint. 5 | * @returns {string|boolean} Returns false if the provided value is invalid, or 6 | * nonexistent. Returns `providedServices` if the query is `provided` or returns 7 | * `consumedServices` if the query is `consumed` 8 | */ 9 | 10 | module.exports = { 11 | schema: { 12 | name: "serviceType", 13 | in: "query", 14 | schema: { 15 | type: "string", 16 | enum: ["consumed", "provided"], 17 | }, 18 | example: "consumed", 19 | allowEmptyValue: true, 20 | deescription: 21 | "Choose whether to display 'consumer' or 'providers' of the specified service.", 22 | }, 23 | logic: (req) => { 24 | // TODO determine if there's a way to indicate this is a required 25 | // field moo 26 | const prov = req.query.serviceType; 27 | 28 | if (prov === undefined) { 29 | return false; 30 | } 31 | 32 | if (prov === "provided") { 33 | return "providedServices"; 34 | } 35 | 36 | if (prov === "consumed") { 37 | return "consumedServices"; 38 | } 39 | 40 | return false; // fallback 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/query_parameters/serviceVersion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function serviceVersion 3 | * @desc Returns the version of whatever service is being requested. 4 | * @param {object} req - The `Request` object inherited from the Express Endpoint. 5 | * @returns {string|boolean} Returns false if the provided value is invalid, or 6 | * nonexistant. Returns the version as a string otherwise. 7 | */ 8 | 9 | module.exports = { 10 | schema: { 11 | name: "serviceVersion", 12 | in: "query", 13 | schema: { 14 | type: "string", 15 | }, 16 | example: "0.0.1", 17 | allowEmptyValue: true, 18 | description: "Filter by a specific version of the 'service'.", 19 | }, 20 | logic: (req) => { 21 | const semver = req.query.serviceVersion; 22 | try { 23 | // Regex matching what's used in query.engine() 24 | const regex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)/; 25 | 26 | // Check if it's a valid semver 27 | return semver.match(regex) !== null ? semver : false; 28 | } catch (err) { 29 | return false; 30 | } 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/query_parameters/shared/owner.js: -------------------------------------------------------------------------------- 1 | const utils = require("../utils.js"); 2 | 3 | // Shared handling of data for the owner query parameter 4 | 5 | // Owner accepts the owner as an argument for things like search, 6 | // as well as a path, for the endpoint `/api/owners/:ownerName` 7 | module.exports = function main(value) { 8 | if (!utils.stringValidation(value)) { 9 | return false; 10 | } 11 | if (value.length === 0) { 12 | return false; 13 | } 14 | 15 | return value; 16 | }; 17 | -------------------------------------------------------------------------------- /src/query_parameters/sort.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function sort 3 | * @desc Parser for the 'sort' query parameter. Defaulting usually to downloads. 4 | * @param {object} req - The `Request` object inherited from the Express endpoint. 5 | * @param {string} [def="downloads"] - The default provided for sort. Allowing 6 | * The search function to use "relevance" instead of the default "downloads". 7 | * @returns {string} Either the user provided 'sort' query parameter, or the default specified. 8 | */ 9 | 10 | module.exports = { 11 | schema: { 12 | name: "sort", 13 | in: "query", 14 | schema: { 15 | type: "string", 16 | enum: ["downloads", "created_at", "updated_at", "stars", "relevance"], 17 | default: "downloads", 18 | }, 19 | example: "downloads", 20 | required: false, 21 | allowEmptyValue: false, 22 | description: "Value to sort search results by.", 23 | }, 24 | logic: (req, def = "downloads") => { 25 | // TODO: Determine if allowing `def` value here makes any sense still 26 | // using sort with a default def value of downloads, means when using the generic sort parameter 27 | // it will default to downloads, but if we pass the default, such as during search we can provide 28 | // the default relevance 29 | const valid = [ 30 | "downloads", 31 | "created_at", 32 | "updated_at", 33 | "stars", 34 | "relevance", 35 | ]; 36 | 37 | const prov = req.query.sort ?? def; 38 | 39 | return valid.includes(prov) ? prov : def; 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/query_parameters/tag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function tag 3 | * @desc Parses the 'tag' query parameter, returning it if valid, otherwise returning ''. 4 | * @param {object} req - The `Request` object inherited from the Express endpoint. 5 | * @returns {string} Returns a valid 'tag' query parameter. Or '' if invalid. 6 | */ 7 | 8 | module.exports = { 9 | schema: { 10 | name: "tag", 11 | in: "query", 12 | schema: { 13 | type: "string", 14 | }, 15 | example: "TODO", 16 | allowEmptyValue: false, 17 | required: false, 18 | description: "TODO", 19 | }, 20 | logic: (req) => { 21 | return typeof req.query.tag !== "string" ? "" : req.query.tag; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/query_parameters/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function stringValidation 3 | * @desc Provides a generic Query Utility that validates if a provided value 4 | * is a string, as well as trimming it to the safe max length of query strings, 5 | * while additionally passing it through the Path Traversal Detection function. 6 | * @param {string} value - The value to check 7 | * @returns {string|boolean} Returns false if any check fails, otherwise returns 8 | * the valid string. 9 | */ 10 | function stringValidation(value) { 11 | const maxLength = 50; 12 | const prov = value; 13 | 14 | if (typeof prov !== "string") { 15 | return false; 16 | } 17 | 18 | return pathTraversalAttempt(prov) ? false : prov.slice(0, maxLength).trim(); 19 | } 20 | 21 | /** 22 | * @function pathTraversalAttempt 23 | * @desc Completes some short checks to determine if the data contains a malicious 24 | * path traversal attempt. Returning a boolean indicating if a path traversal attempt 25 | * exists in the data. 26 | * @param {string} data - The data to check for possible malicious data. 27 | * @returns {boolean} True indicates a path traversal attempt was found. False otherwise. 28 | */ 29 | function pathTraversalAttempt(data) { 30 | // This will use several methods to check for the possibility of an attempted path traversal attack. 31 | 32 | // The definitions here are based off GoPage checks. 33 | // https://github.com/confused-Techie/GoPage/blob/main/src/pkg/universalMethods/universalMethods.go 34 | // But we leave out any focused on defended against URL Encoded values, since this has already been decoded. 35 | // const checks = [ 36 | // /\.{2}\//, //unixBackNav 37 | // /\.{2}\\/, //unixBackNavReverse 38 | // /\.{2}/, //unixParentCatchAll 39 | // ]; 40 | 41 | // Combine the 3 regex into one: https://regex101.com/r/CgcZev/1 42 | const check = /\.{2}(?:[/\\])?/; 43 | return data.match(check) !== null; 44 | } 45 | 46 | module.exports = { 47 | stringValidation, 48 | pathTraversalAttempt, 49 | }; 50 | -------------------------------------------------------------------------------- /src/query_parameters/versionName.js: -------------------------------------------------------------------------------- 1 | const engine = require("./engine.js").logic; 2 | 3 | module.exports = { 4 | schema: { 5 | name: "versionName", 6 | in: "path", 7 | schema: { 8 | type: 'string"', 9 | }, 10 | required: true, 11 | allowEmptyValue: false, 12 | example: "1.0.0", 13 | description: "The version of the package to access.", 14 | }, 15 | logic: engine, 16 | }; 17 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | * @desc The initializer of `main.js` starting up the Express Server, and setting the port 4 | * to listen on. As well as handling a graceful shutdown of the server. 5 | */ 6 | 7 | const app = require("./setupEndpoints.js"); 8 | const { port } = require("./config.js").getConfig(); 9 | const logger = require("./logger.js"); 10 | const database = require("./database/_export.js"); 11 | 12 | if (process.env.PULSAR_STATUS === "dev") { 13 | logger.generic(3, "Pulsar Server is in Development Mode!"); 14 | } 15 | 16 | const serve = app.listen(port, () => { 17 | logger.generic(4, `Pulsar Server Listening on port ${port}`); 18 | }); 19 | 20 | process.on("SIGTERM", async () => { 21 | await exterminate("SIGTERM"); 22 | }); 23 | 24 | process.on("SIGINT", async () => { 25 | await exterminate("SIGINT"); 26 | }); 27 | 28 | /** 29 | * @async 30 | * @function exterminate 31 | * @desc This is called when the server process receives a `SIGINT` or `SIGTERM` signal. 32 | * Which this will then handle closing the server listener, as well as calling `data.Shutdown`. 33 | * @param {string} callee - Simply a way to better log what called the server to shutdown. 34 | */ 35 | async function exterminate(callee) { 36 | console.log(`${callee} signal received: closing HTTP server.`); 37 | await database.shutdownSQL(); 38 | console.log("Exiting..."); 39 | serve.close(() => { 40 | console.log("HTTP Server Closed."); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/vcs_providers/README.md: -------------------------------------------------------------------------------- 1 | # Version Control System Providers 2 | 3 | The Classes within this folder allow for `vcs.js` to interact with as many VCS services as we can, while ensuring users are able to publish, download, and install the same way from each one. 4 | 5 | This is the groundwork to allow us to support not just GitHub for hosting and publishing packages. 6 | 7 | > Note: These are quick notes during the development of this feature and may not reflect current usage. Please read while also thoroughly studying the source code or documentation generated from it. 8 | 9 | Each VCS Provider Class can use the `git.js` or `Git` Class to assist in structure, and fleshing our some authentication interactions that will happen frequently. 10 | 11 | Each VCS provider needs to publicly provide the following capabilities: 12 | * `ownership` A way to confirm if a user has write permission to a specific package on each VCS Service. 13 | * `readme` A way to provide the Markdown text of the package's main readme, whatever the equivalent would be on each service. 14 | * `tags` A way to provide all tags of a package. 15 | * `packageJSON` A way to provide the `package.json` that this package uses once installed. 16 | * `exists` A way to confirm if any arbitrary package on the VCS Service exists, and or is publicly available. 17 | 18 | Since the majority of the backend does not need deep integration with each VCS Service on it's own, `vcs.js` will export the following capabilities. 19 | 20 | * `ownership` Should essentially redirect to the relevant VCS provider. 21 | * `newPackageData` The function that should be used during `postPackages` that will provide __All__ data available for an individual package. This includes utilitizing the following: 22 | - `readme` 23 | - `tags` 24 | - `packageJSON` 25 | * `newVersionData` The function that should be used during `postPackagesVersion` to provide __All__ data available for an individual package with some modifications. Utilizes: 26 | - `readme` 27 | - `tags` 28 | - `packageJSON` 29 | * `determineProvider` While intended to be used as an internal function, it can reliably determine which VCS service and thus provider is intended to be used for a package. 30 | -------------------------------------------------------------------------------- /tests/database/fixtures/git.createPackage_returns/valid_multi_version.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "publish-test-valid-multi-version", 3 | repository: { 4 | type: "git", 5 | url: "https://github.com/pulsar-edit/test", 6 | }, 7 | creation_method: "Test Package", 8 | readme: "This is a readme!", 9 | owner: "pulsar-edit", 10 | metadata: { 11 | name: "publish-test-valid-multi-version", 12 | license: "MIT", 13 | version: "1.0.3", 14 | }, 15 | releases: { 16 | latest: "1.0.3", 17 | }, 18 | versions: { 19 | "1.0.0": { 20 | name: "publish-test-valid-multi-version", 21 | version: "1.0.0", 22 | tarball_url: "https://nowhere.com", 23 | sha: "12345", 24 | }, 25 | "1.0.1": { 26 | name: "publish-test-valid-multi-version", 27 | version: "1.0.1", 28 | tarball_url: "https://nowhere.com", 29 | sha: "12345", 30 | }, 31 | "1.0.2": { 32 | name: "publish-test-valid-multi-version", 33 | version: "1.0.2", 34 | tarball_url: "https://nowhere.com", 35 | sha: "12344", 36 | }, 37 | "1.0.3": { 38 | name: "publish-test-valid-multi-version", 39 | version: "1.0.3", 40 | tarball_url: "https://nowhere.com", 41 | sha: "12345", 42 | }, 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /tests/database/fixtures/git.createPackage_returns/valid_one_version.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "publish-test-valid", 3 | repository: { 4 | type: "git", 5 | url: "https://github.com/pulsar-edit/test", 6 | }, 7 | creation_method: "Test Package", 8 | readme: "This is a readme!", 9 | metadata: { 10 | name: "publish-test-valid", 11 | license: "MIT", 12 | version: "1.0.0", 13 | }, 14 | owner: "confused-Techie", 15 | releases: { 16 | latest: "1.0.0", 17 | }, 18 | versions: { 19 | "1.0.0": { 20 | name: "publish-test-valid", 21 | version: "1.0.0", 22 | tarball_url: "https://nowhere.com", 23 | sha: "12345", 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /tests/database/fixtures/lifetime/package-a.js: -------------------------------------------------------------------------------- 1 | const createPack = { 2 | name: "package-a-lifetime", 3 | repository: { 4 | type: "git", 5 | url: "https://github.com/pulsar-edit/package-a-lifetime", 6 | }, 7 | owner: "pulsar-edit", 8 | creation_method: "Test Package", 9 | readme: "This is a readme!", 10 | metadata: { 11 | name: "package-a-lifetime", 12 | license: "MIT", 13 | version: "1.0.0", 14 | }, 15 | releases: { 16 | latest: "1.0.0", 17 | }, 18 | versions: { 19 | "1.0.0": { 20 | name: "package-a-lifetime", 21 | version: "1.0.0", 22 | tarball_url: "https://nowhere.com", 23 | sha: "12345", 24 | }, 25 | }, 26 | }; 27 | 28 | const addVersion = (v) => { 29 | return { 30 | name: "package-a-lifetime", 31 | version: v, 32 | description: "A package.json description", 33 | license: "MIT", 34 | engines: { atom: "*" }, 35 | }; 36 | }; 37 | 38 | const packageDataForVersion = (v) => { 39 | return { 40 | name: "package-a-lifetime", 41 | repository: { 42 | type: "git", 43 | url: "https://github.com/pulsar-edit/package-a-lifetime", 44 | }, 45 | readme: "This is a readme!", 46 | metadata: v, 47 | }; 48 | }; 49 | 50 | module.exports = { 51 | createPack, 52 | addVersion, 53 | packageDataForVersion, 54 | }; 55 | -------------------------------------------------------------------------------- /tests/database/fixtures/lifetime/user-a.js: -------------------------------------------------------------------------------- 1 | const userObj = { 2 | username: "user-a-test", 3 | node_id: "fakeNodeID", 4 | avatar: "https://nowhere.com", 5 | }; 6 | 7 | module.exports = { 8 | userObj, 9 | }; 10 | -------------------------------------------------------------------------------- /tests/full/fixtures/a-pulsar-package/match.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "a-pulsar-package", 3 | owner: "confused-Techie", 4 | readme: expect.stringContaining("I'm a readme!"), 5 | metadata: { 6 | dist: { 7 | sha: "09f", 8 | tarball: 9 | "https://api.github.com/repos/confused-Techie/a-pulsar-package/tarball/refs/tags/v1.0.0", 10 | }, 11 | main: "./lib/main.js", 12 | name: "a-pulsar-package", 13 | engines: { atom: "*" }, 14 | license: "MIT", 15 | version: "1.0.0", 16 | repository: "https://github.com/confused-Techie/a-pulsar-package", 17 | description: "A package for stuff.", 18 | }, 19 | releases: { latest: "1.0.0" }, 20 | versions: { 21 | "1.0.0": { 22 | dist: { 23 | tarball: expect.stringContaining( 24 | "/api/packages/a-pulsar-package/versions/1.0.0/tarball" 25 | ), 26 | }, 27 | main: "./lib/main.js", 28 | name: "a-pulsar-package", 29 | license: "MIT", 30 | version: "1.0.0", 31 | repository: "https://github.com/confused-Techie/a-pulsar-package", 32 | description: "A package for stuff.", 33 | }, 34 | }, 35 | repository: { 36 | url: "https://github.com/confused-Techie/a-pulsar-package", 37 | type: "git", 38 | }, 39 | creation_method: "User Made Package", 40 | downloads: "0", 41 | stargazers_count: "0", 42 | badges: [{ title: "Made for Pulsar!", type: "success" }], 43 | }; 44 | -------------------------------------------------------------------------------- /tests/full/fixtures/a-pulsar-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a-pulsar-package", 3 | "version": "1.0.0", 4 | "description": "A package for stuff.", 5 | "main": "./lib/main.js", 6 | "engines": { 7 | "atom": "*" 8 | }, 9 | "repository": "https://github.com/confused-Techie/a-pulsar-package", 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /tests/full/fixtures/a-pulsar-package/readme.md: -------------------------------------------------------------------------------- 1 | # a-pulsar-package 2 | 3 | I'm a readme! 4 | -------------------------------------------------------------------------------- /tests/full/fixtures/a-pulsar-package/tags.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | name: "v1.0.0", 4 | tarball_url: 5 | "https://api.github.com/repos/confused-Techie/a-pulsar-package/tarball/refs/tags/v1.0.0", 6 | commit: { 7 | sha: "09f", 8 | url: "https://api.github.com/repos/confused-Techie/a-pulsar-package/commits/09f", 9 | }, 10 | }, 11 | ]; 12 | -------------------------------------------------------------------------------- /tests/full/fixtures/b-pulsar-package/match.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "b-pulsar-package", 3 | owner: "confused-Techie", 4 | readme: expect.stringContaining("I'm a readme!"), 5 | metadata: { 6 | dist: { 7 | sha: "09f", 8 | tarball: 9 | "https://api.github.com/repos/confused-Techie/b-pulsar-package/tarball/refs/tags/v2.0.0", 10 | }, 11 | main: "./lib/main.js", 12 | name: "b-pulsar-package", 13 | engines: { atom: "*" }, 14 | license: "MIT", 15 | version: "2.0.0", 16 | repository: "https://github.com/confused-Techie/b-pulsar-package", 17 | description: "A package for stuff.", 18 | }, 19 | releases: { latest: "2.0.0" }, 20 | versions: { 21 | "2.0.0": { 22 | dist: { 23 | tarball: expect.stringContaining( 24 | "/api/packages/b-pulsar-package/versions/2.0.0/tarball" 25 | ), 26 | }, 27 | main: "./lib/main.js", 28 | name: "b-pulsar-package", 29 | license: "MIT", 30 | version: "2.0.0", 31 | repository: "https://github.com/confused-Techie/b-pulsar-package", 32 | description: "A package for stuff.", 33 | }, 34 | "1.0.0": { 35 | dist: { 36 | tarball: expect.stringContaining( 37 | "/api/packages/b-pulsar-package/versions/1.0.0/tarball" 38 | ), 39 | }, 40 | //main: "./lib/main.js", 41 | // Since building the package.json of v1.0.0 is an absent build, we 42 | // can't expect it to know what the 'main' entry may have been 43 | name: "b-pulsar-package", 44 | license: "MIT", 45 | version: "1.0.0", 46 | repository: "https://github.com/confused-Techie/b-pulsar-package", 47 | description: "A package for stuff.", 48 | }, 49 | }, 50 | repository: { 51 | url: "https://github.com/confused-Techie/b-pulsar-package", 52 | type: "git", 53 | }, 54 | creation_method: "User Made Package", 55 | downloads: "0", 56 | stargazers_count: "0", 57 | badges: [{ title: "Made for Pulsar!", type: "success" }], 58 | }; 59 | -------------------------------------------------------------------------------- /tests/full/fixtures/b-pulsar-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "b-pulsar-package", 3 | "version": "2.0.0", 4 | "description": "A package for stuff.", 5 | "main": "./lib/main.js", 6 | "engines": { 7 | "atom": "*" 8 | }, 9 | "repository": "https://github.com/confused-Techie/b-pulsar-package", 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /tests/full/fixtures/b-pulsar-package/readme.md: -------------------------------------------------------------------------------- 1 | # b-pulsar-package 2 | 3 | I'm a readme! 4 | -------------------------------------------------------------------------------- /tests/full/fixtures/b-pulsar-package/tags.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | name: "v1.0.0", 4 | tarball_url: 5 | "https://api.github.com/repos/confused-Techie/b-pulsar-package/tarball/refs/tags/v1.0.0", 6 | commit: { 7 | sha: "09f", 8 | url: "https://api.github.com/repos/confused-Techie/b-pulsar-package/commits/09f", 9 | }, 10 | }, 11 | { 12 | name: "v2.0.0", 13 | tarball_url: 14 | "https://api.github.com/repos/confused-Techie/b-pulsar-package/tarball/refs/tags/v2.0.0", 15 | commit: { 16 | sha: "09f", 17 | url: "https://api.github.com/repos/confused-Techie/b-pulsar-package/commits/09f", 18 | }, 19 | }, 20 | ]; 21 | -------------------------------------------------------------------------------- /tests/full/fixtures/c-pulsar-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "c-pulsar-package", 3 | "version": "2.0.0", 4 | "description": "A package for stuff", 5 | "main": "./lib/main.js", 6 | "engines": { 7 | "atom": "*" 8 | }, 9 | "repository": "https://github.com/confused-Techie/c-pulsar-package", 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /tests/full/fixtures/c-pulsar-package/readme.md: -------------------------------------------------------------------------------- 1 | # b-pulsar-package 2 | 3 | I'm a readme! 4 | -------------------------------------------------------------------------------- /tests/full/fixtures/c-pulsar-package/tags.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | name: "v1.0.0", 4 | tarball_url: 5 | "https://api.github.com/repos/confused-Techie/c-pulsar-package/tarball/refs/tags/v1.0.0", 6 | commit: { 7 | sha: "09f", 8 | url: "https://api.github.com/repos/confused-Techie/c-pulsar-package/commits/09f", 9 | }, 10 | }, 11 | // The `package.json` declares it's version as '2.0.0' but we only provide 12 | // tags for version v1.0.0 in this way we want to make sure that this publication 13 | // fails! 14 | ]; 15 | -------------------------------------------------------------------------------- /tests/full/fixtures/d-pulsar-package/initial-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d-pulsar-package", 3 | "version": "1.0.0", 4 | "description": "A new package for stuff", 5 | "main": "./lib/main.js", 6 | "engines": { 7 | "atom": "*" 8 | }, 9 | "repository": "https://github.com/confused-Techie/d-pulsar-package", 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /tests/full/fixtures/d-pulsar-package/initial-tags.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | name: "v1.0.0", 4 | tarball_url: 5 | "https://api.github.com/repos/confused-Techie/d-pulsar-package/tarball/refs/tags/v1.0.0", 6 | commit: { 7 | sha: "09f", 8 | url: "https://api.github.com/repos/confused-Techie/d-pulsar-package/commits/09f", 9 | }, 10 | }, 11 | ]; 12 | -------------------------------------------------------------------------------- /tests/full/fixtures/d-pulsar-package/match.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "d-pulsar-package", 3 | owner: "confused-Techie", 4 | readme: expect.stringContaining("I'm a readme!"), 5 | metadata: { 6 | main: "./lib/main.js", 7 | name: "d-pulsar-package", 8 | engines: { atom: "*" }, 9 | license: "MIT", 10 | version: "2.0.0", 11 | repository: "https://github.com/confused-Techie/d-pulsar-package", 12 | description: "An old package for stuff", 13 | }, 14 | releases: { latest: "2.0.0" }, 15 | versions: { 16 | "2.0.0": { 17 | dist: { 18 | tarball: expect.stringContaining( 19 | "/api/packages/d-pulsar-package/versions/2.0.0/tarball" 20 | ), 21 | }, 22 | main: "./lib/main.js", 23 | name: "d-pulsar-package", 24 | license: "MIT", 25 | version: "2.0.0", 26 | repository: "https://github.com/confused-Techie/d-pulsar-package", 27 | description: "An old package for stuff", 28 | }, 29 | "1.0.0": { 30 | dist: { 31 | tarball: expect.stringContaining( 32 | "/api/packages/d-pulsar-package/versions/1.0.0/tarball" 33 | ), 34 | }, 35 | main: "./lib/main.js", 36 | name: "d-pulsar-package", 37 | license: "MIT", 38 | version: "1.0.0", 39 | repository: "https://github.com/confused-Techie/d-pulsar-package", 40 | description: "A new package for stuff", 41 | }, 42 | }, 43 | repository: { 44 | url: "https://github.com/confused-Techie/d-pulsar-package", 45 | type: "git", 46 | }, 47 | downloads: "0", 48 | stargazers_count: "0", 49 | badges: [{ title: "Made for Pulsar!", type: "success" }], 50 | }; 51 | -------------------------------------------------------------------------------- /tests/full/fixtures/d-pulsar-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d-pulsar-package", 3 | "version": "2.0.0", 4 | "description": "An old package for stuff", 5 | "main": "./lib/main.js", 6 | "engines": { 7 | "atom": "*" 8 | }, 9 | "repository": "https://github.com/confused-Techie/d-pulsar-package", 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /tests/full/fixtures/d-pulsar-package/readme.md: -------------------------------------------------------------------------------- 1 | # d-pulsar-package 2 | 3 | I'm a readme! 4 | -------------------------------------------------------------------------------- /tests/full/fixtures/d-pulsar-package/tags.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | name: "v2.0.0", 4 | tarball_url: 5 | "https://api.github.com/repos/confused-Techie/d-pulsar-package/tarball/refs/tags/v2.0.0", 6 | commit: { 7 | sha: "09f", 8 | url: "https://api.github.com/repos/confused-Techie/d-pulsar-package/commits/09f", 9 | }, 10 | }, 11 | { 12 | name: "v1.0.0", 13 | tarball_url: 14 | "https://api.github.com/repos/confused-Techie/d-pulsar-package/tarball/refs/tags/v1.0.0", 15 | commit: { 16 | sha: "09f", 17 | url: "https://api.github.com/repos/confused-Techie/d-pulsar-package/commits/09f", 18 | }, 19 | }, 20 | ]; 21 | -------------------------------------------------------------------------------- /tests/helpers/handlers.setup.jest.js: -------------------------------------------------------------------------------- 1 | // This module will provide a setup for the *.handler.integration.test.js files. 2 | // This mainly means to properly set timeouts, and to ensure that required 3 | // env vars are set properly. 4 | 5 | //jest.setTimeout(3000000); 6 | 7 | const dbUrl = process.env.DATABASE_URL; 8 | // this gives us something like postgres://test-user@localhost:5432/test-db 9 | // We then need to map these values to where the API server expects, 10 | const dbUrlReg = /postgres:\/\/([\/\S]+)@([\/\S]+):(\d+)\/([\/\S]+)/; 11 | const dbUrlParsed = dbUrlReg.exec(dbUrl); 12 | 13 | // set the parsed URL as proper env 14 | process.env.DB_HOST = dbUrlParsed[2]; 15 | process.env.DB_USER = dbUrlParsed[1]; 16 | process.env.DB_DB = dbUrlParsed[4]; 17 | process.env.DB_PORT = dbUrlParsed[3]; 18 | 19 | // Then since we want to make sure we don't initialize the config module, before we have set our values, 20 | // we will define our own port to use here. 21 | process.env.PORT = 8080; 22 | 23 | // Now any tests that have this called prior to the test being run can `require("../main.js")` 24 | // without any issue 25 | 26 | // But now lets setup some global objects for tests to rely on 27 | 28 | global.msg = { 29 | badRepoJSON: 30 | "That repo does not exist, isn't an atom package, or atombot does not have access.", 31 | badAuth: 32 | "Requires authentication. Please update your token if you haven't done so recently.", 33 | notSupported: "While under development this feature is not supported.", 34 | publishPackageExists: "A Package by that name already exists.", 35 | notFound: "Not Found", 36 | notFoundSite: "This is a standin for the proper site wide 404 page.", 37 | serverError: "Application Error", 38 | badPackageJSON: "The package.json at owner/repo isn't valid.", 39 | }; 40 | -------------------------------------------------------------------------------- /tests/http/getOwnersOwnerName.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../src/controllers/getOwnersOwnerName.js"); 2 | const database = require("../../src/database/_export.js"); 3 | const context = require("../../src/context.js"); 4 | 5 | const genPackage = require("../helpers/package.jest.js"); 6 | 7 | describe("Behaves as expected", () => { 8 | test("Calls the correct function", async () => { 9 | const localContext = context; 10 | const spy = jest.spyOn(localContext.database, "getSortedPackages"); 11 | 12 | await endpoint.logic({}, localContext); 13 | 14 | expect(spy).toBeCalledTimes(1); 15 | 16 | spy.mockClear(); 17 | }); 18 | 19 | test("Returns empty array with no matching results", async () => { 20 | const sso = await endpoint.logic( 21 | { 22 | owner: "i-dont-exist", 23 | page: "1", 24 | sort: "downloads", 25 | direction: "desc", 26 | }, 27 | context 28 | ); 29 | 30 | expect(sso.ok).toBe(true); 31 | expect(sso.content).toBeArray(); 32 | expect(sso.content.length).toBe(0); 33 | }); 34 | 35 | test("Returns package with matching owner entry", async () => { 36 | await database.insertNewPackage( 37 | genPackage("https://github.com/pulsar-cooperative/get-owner-test") 38 | ); 39 | 40 | const sso = await endpoint.logic( 41 | { 42 | owner: "pulsar-cooperative", 43 | page: 1, 44 | sort: "downloads", 45 | direction: "desc", 46 | }, 47 | context 48 | ); 49 | 50 | expect(sso.ok).toBe(true); 51 | expect(sso.content[0].name).toBe("get-owner-test"); 52 | expect(sso.content).toBeArray(); 53 | expect(sso.content.length).toBe(1); 54 | expect(sso).toMatchEndpointSuccessObject(endpoint); 55 | 56 | await database.removePackageByName("get-owner-test", true); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/http/getPackagesFeatured.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../src/controllers/getPackagesFeatured.js"); 2 | const database = require("../../src/database/_export.js"); 3 | const context = require("../../src/context.js"); 4 | 5 | const genPackage = require("../helpers/package.jest.js"); 6 | 7 | describe("Behaves as expected", () => { 8 | test("Calls the correct function", async () => { 9 | const localContext = context; 10 | const spy = jest.spyOn(localContext.database, "getFeaturedPackages"); 11 | 12 | await endpoint.logic({}, localContext); 13 | 14 | expect(spy).toBeCalledTimes(1); 15 | 16 | spy.mockClear(); 17 | }); 18 | 19 | test("Returns not found with no packages present", async () => { 20 | const sso = await endpoint.logic({}, context); 21 | 22 | expect(sso.ok).toBe(false); 23 | expect(sso.content.short).toBe("not_found"); 24 | }); 25 | 26 | test("Returns proper data on success", async () => { 27 | await database.insertNewPackage( 28 | genPackage( 29 | // We know a currently featured package is 'x-terminal-reloaded' 30 | "https://github.com/Spiker985/x-terminal-reloaded", 31 | { 32 | versions: ["1.1.0", "1.0.0"], 33 | } 34 | ) 35 | ); 36 | 37 | const sso = await endpoint.logic({}, context); 38 | 39 | expect(sso.ok).toBe(true); 40 | expect(sso.content).toBeArray(); 41 | expect(sso.content.length).toBe(1); 42 | expect(sso.content[0].name).toBe("x-terminal-reloaded"); 43 | expect(sso).toMatchEndpointSuccessObject(endpoint); 44 | 45 | await database.removePackageByName("x-terminal-reloaded", true); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/http/getPackagesPackageName.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../src/controllers/getPackagesPackageName.js"); 2 | const database = require("../../src/database/_export.js"); 3 | const context = require("../../src/context.js"); 4 | 5 | const genPackage = require("../helpers/package.jest.js"); 6 | 7 | describe("Behaves as expected", () => { 8 | test("Returns 'not_found' when package doesn't exist", async () => { 9 | const sso = await endpoint.logic( 10 | { 11 | engine: false, 12 | packageName: "anything", 13 | }, 14 | context 15 | ); 16 | 17 | expect(sso.ok).toBe(false); 18 | expect(sso.content.short).toBe("not_found"); 19 | }); 20 | 21 | test("Returns package on success", async () => { 22 | await database.insertNewPackage( 23 | genPackage("https://github.com/confused-Techie/get-package-test", { 24 | versions: ["1.1.0", "1.0.0"], 25 | }) 26 | ); 27 | 28 | const sso = await endpoint.logic( 29 | { 30 | engine: false, 31 | packageName: "get-package-test", 32 | }, 33 | context 34 | ); 35 | 36 | expect(sso.ok).toBe(true); 37 | expect(sso.content.name).toBe("get-package-test"); 38 | expect(sso.content.owner).toBe("confused-Techie"); 39 | expect(sso).toMatchEndpointSuccessObject(endpoint); 40 | await database.removePackageByName("get-package-test", true); 41 | }); 42 | 43 | test("Returns a bundled package without it existing in the database", async () => { 44 | const sso = await endpoint.logic( 45 | { 46 | engine: false, 47 | packageName: "settings-view", 48 | }, 49 | context 50 | ); 51 | 52 | expect(sso.ok).toBe(true); 53 | expect(sso.content.name).toBe("settings-view"); 54 | expect(sso.content.owner).toBe("pulsar-edit"); 55 | expect(sso.content.repository.url).toBe( 56 | "https://github.com/pulsar-edit/pulsar" 57 | ); 58 | expect(sso).toMatchEndpointSuccessObject(endpoint); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/http/getRoot.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../src/controllers/getRoot.js"); 2 | const context = require("../../src/context.js"); 3 | 4 | describe("Behaves as expected", () => { 5 | test("Should respond with an HTML document", async () => { 6 | const sso = await endpoint.logic({}, context); 7 | 8 | expect(sso.ok).toBe(true); 9 | expect(sso.content).toEqual( 10 | expect.stringContaining("Server is up and running Version") 11 | ); 12 | }); 13 | }); 14 | 15 | describe("HTTP Handling works", () => { 16 | test("Calls the right function", async () => { 17 | const request = require("supertest"); 18 | const app = require("../../src/setupEndpoints.js"); 19 | 20 | const spy = jest.spyOn(endpoint, "logic"); 21 | 22 | const res = await request(app).get("/"); 23 | 24 | expect(spy).toBeCalledTimes(1); 25 | 26 | spy.mockClear(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/http/getThemesFeatured.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../src/controllers/getThemesFeatured.js"); 2 | const database = require("../../src/database/_export.js"); 3 | const context = require("../../src/context.js"); 4 | 5 | const genPackage = require("../helpers/package.jest.js"); 6 | 7 | describe("Behaves as expected", () => { 8 | test("Calls the correct function", async () => { 9 | const localContext = context; 10 | const spy = jest.spyOn(localContext.database, "getFeaturedThemes"); 11 | 12 | await endpoint.logic({}, localContext); 13 | 14 | expect(spy).toBeCalledTimes(1); 15 | 16 | spy.mockClear(); 17 | }); 18 | 19 | test("Returns not found with no packages present", async () => { 20 | const sso = await endpoint.logic({}, context); 21 | 22 | expect(sso.ok).toBe(false); 23 | expect(sso.content.short).toBe("not_found"); 24 | }); 25 | 26 | test("Returns proper data on success", async () => { 27 | await database.insertNewPackage( 28 | // We know a currently featured package is 'atom-material-ui' 29 | genPackage("https://github.com/confused-Techie/atom-material-ui", { 30 | extraVersionData: { 31 | theme: "ui", 32 | }, 33 | }) 34 | ); 35 | 36 | const sso = await endpoint.logic({}, context); 37 | 38 | expect(sso.ok).toBe(true); 39 | expect(sso.content).toBeArray(); 40 | expect(sso.content.length).toBe(1); 41 | expect(sso.content[0].name).toBe("atom-material-ui"); 42 | expect(sso).toMatchEndpointSuccessObject(endpoint); 43 | await database.removePackageByName("atom-material-ui", true); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/http/getUpdates.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../src/controllers/getUpdates.js"); 2 | const context = require("../../src/context.js"); 3 | 4 | describe("Behaves as expected", () => { 5 | test("Returns properly", async () => { 6 | const sso = await endpoint.logic({}, context); 7 | 8 | expect(sso.ok).toBe(false); 9 | expect(sso.short).toBe("not_supported"); 10 | }); 11 | }); 12 | 13 | describe("HTTP Handling works", () => { 14 | test("Calls the right function", async () => { 15 | const request = require("supertest"); 16 | const app = require("../../src/setupEndpoints.js"); 17 | 18 | const spy = jest.spyOn(endpoint, "logic"); 19 | 20 | const res = await request(app).get("/api/updates"); 21 | 22 | expect(spy).toBeCalledTimes(1); 23 | 24 | spy.mockClear(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/http/getUsersLogin.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../src/controllers/getUsersLogin.js"); 2 | const database = require("../../src/database/_export.js"); 3 | const context = require("../../src/context.js"); 4 | const userObject = require("../models/userObjectPublic.js"); 5 | 6 | describe("Behaves as expected", () => { 7 | test("Returns bad SSO on failure", async () => { 8 | const userName = "our-test-user"; 9 | 10 | const res = await endpoint.logic({ login: userName }, context); 11 | 12 | expect(res.ok).toBe(false); 13 | expect(res.content.short).toBe("not_found"); 14 | }); 15 | 16 | test("Returns Correct SSO on Success", async () => { 17 | const userObj = userObject.example; 18 | userObj.username = "our-test-user"; 19 | 20 | // First we add a new fake user 21 | await database.insertNewUser(userObj.username, "id", userObj.avatar); 22 | 23 | const res = await endpoint.logic( 24 | { 25 | login: userObj.username, 26 | }, 27 | context 28 | ); 29 | 30 | expect(res.ok).toBe(true); 31 | // First make sure the user object matches expectations broadly, then specifically 32 | expect(res.content.username).toBe(userObj.username); 33 | expect(res.content.avatar).toBe(userObj.avatar); 34 | expect(res).toMatchEndpointSuccessObject(endpoint); 35 | // TODO delete once there's a method to do so 36 | }); 37 | }); 38 | 39 | describe("HTTP Handling works", () => { 40 | test("Calls the right function", async () => { 41 | const request = require("supertest"); 42 | const app = require("../../src/setupEndpoints.js"); 43 | 44 | const spy = jest.spyOn(endpoint, "logic"); 45 | 46 | const res = await request(app).get("/api/users/confused-Techie"); 47 | 48 | expect(spy).toBeCalledTimes(1); 49 | 50 | spy.mockClear(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/http/login.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../../src/setupEndpoints.js"); 3 | 4 | describe("Get /api/login", () => { 5 | test("Returns proper Status Code", async () => { 6 | const res = await request(app).get("/api/login"); 7 | expect(res).toHaveHTTPCode(302); 8 | }); 9 | // As for testing the rest of the behavior is near impossible. 10 | // Since it relies so heavily on GitHub. 11 | }); 12 | -------------------------------------------------------------------------------- /tests/http/postPackagesPackageNameVersions.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../src/controllers/postPackagesPackageNameVersions.js"); 2 | const database = require("../../src/database/_export.js"); 3 | const context = require("../../src/context.js"); 4 | 5 | describe("POST /api/packages/:packageName/versions", () => { 6 | test("Fails with bad auth if given a bad auth", async () => { 7 | const localContext = context; 8 | localContext.auth.verifyAuth = () => { 9 | return { 10 | ok: false, 11 | short: "unauthorized", 12 | context: "Bad Auth Mock Return", 13 | }; 14 | }; 15 | 16 | const sso = await endpoint.logic({}, localContext); 17 | 18 | expect(sso.ok).toBe(false); 19 | expect(sso.short).toBe("unauthorized"); 20 | }); 21 | 22 | // This is where the original tests ended here 23 | test.todo("Write the tests that are now possible"); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/http/stars.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../../src/setupEndpoints.js"); 3 | 4 | const { authMock } = require("../helpers/httpMock.helper.jest.js"); 5 | 6 | let tmpMock; 7 | 8 | describe("GET /api/stars", () => { 9 | test("Returns Unauthenticated Status Code for Invalid User", async () => { 10 | tmpMock = authMock({ 11 | ok: false, 12 | short: "unauthorized", 13 | content: "Bad Auth Mock Return for Dev User", 14 | }); 15 | 16 | const res = await request(app) 17 | .get("/api/stars") 18 | .set("Authorization", "invalid"); 19 | expect(res).toHaveHTTPCode(401); 20 | expect(res.body.message).toEqual( 21 | "Unauthorized: Please update your token if you haven't done so recently." 22 | ); 23 | 24 | tmpMock.mockClear(); 25 | }); 26 | 27 | test("Valid User with No Stars Returns array", async () => { 28 | tmpMock = authMock({ 29 | ok: true, 30 | content: { 31 | token: "no-star-token", 32 | id: 342342, 33 | node_id: "no-star-test-user", 34 | username: "no-star-test-user-node-id", 35 | avatar: "https://domain.org", 36 | }, 37 | }); 38 | 39 | const res = await request(app) 40 | .get("/api/stars") 41 | .set("Authorization", "no-star-token"); 42 | expect(res.body).toBeArray(); 43 | expect(res.body.length).toEqual(0); 44 | expect(res).toHaveHTTPCode(200); 45 | 46 | tmpMock.mockClear(); 47 | }); 48 | 49 | test.skip("Valid User with Stars Returns 200 Status Code", async () => { 50 | tmpMock = authMock({ 51 | ok: true, 52 | content: { 53 | token: "all-star-token", 54 | id: 2222, 55 | node_id: "many-star-user-node-id", 56 | username: "many-star-user", 57 | avatar: "https://domain.org", 58 | }, 59 | }); 60 | 61 | const res = await request(app) 62 | .get("/api/stars") 63 | .set("Authorization", "all-star-token"); 64 | expect(res).toHaveHTTPCode(200); 65 | expect(res.body).toBeArray(); 66 | expect(res.body.length).toBeGreaterThan(0); 67 | 68 | tmpMock.mockClear(); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tests/models/message.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | schema: { 3 | description: 4 | "A generic object that could contain status information or error messages.", 5 | type: "object", 6 | required: ["message"], 7 | properties: { 8 | message: { 9 | type: "string", 10 | }, 11 | }, 12 | }, 13 | example: { 14 | message: "This is some message content.", 15 | }, 16 | test: Joi.object({ 17 | message: Joi.string().required(), 18 | }), 19 | }; 20 | -------------------------------------------------------------------------------- /tests/models/packageObjectFullArray.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | schema: { 3 | type: "array", 4 | items: { 5 | $ref: "#/components/schemas/packageObjectFull", 6 | }, 7 | }, 8 | example: [require("./packageObjectFull.js").example], 9 | test: Joi.array().items(require("./packageObjectFull.js").test).required(), 10 | }; 11 | -------------------------------------------------------------------------------- /tests/models/packageObjectJSON.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | schema: { 3 | description: 4 | "The `package.json` of a package with an added `dist.tarball` object.", 5 | type: "object", 6 | required: ["name", "version", "engines", "dist"], 7 | properties: { 8 | name: { type: "string" }, 9 | version: { type: "object" }, 10 | engines: { type: "object" }, 11 | dist: { type: "object" }, 12 | }, 13 | }, 14 | example: { 15 | // This is the full return of `language-robots-txt` 16 | // Notice how some extra information may exist, that is present in the package's 17 | // `package.json`, the schema and test only references required fields 18 | name: "language-robots-txt", 19 | author: "confused-Techie", 20 | license: "MIT", 21 | scripts: { 22 | test: "pulsar --test spec", 23 | }, 24 | version: "1.0.8", 25 | keywords: ["text-mate", "pulsar-package", "pulsar-edit"], 26 | repository: "https://github.com/confused-Techei/language-robots-txt", 27 | description: "Robots.txt Syntax Highlighting in Pulsar", 28 | dist: { 29 | tarball: 30 | "https://api.pulsar-edit.dev/api/packages/language-robots-txt/versions/1.0.8/tarball", 31 | }, 32 | }, 33 | test: Joi.object({ 34 | name: Joi.string().required(), 35 | version: Joi.string().required(), 36 | engines: Joi.object({ 37 | atom: Joi.string().required(), 38 | }).required(), 39 | dist: Joi.object({ 40 | tarball: Joi.string().required(), 41 | }).required(), 42 | }).required(), 43 | }; 44 | -------------------------------------------------------------------------------- /tests/models/packageObjectShortArray.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | schema: { 3 | type: "array", 4 | items: { 5 | $ref: "#/components/schemas/packageObjectShort", 6 | }, 7 | }, 8 | example: [require("./packageObjectShort.js").example], 9 | test: Joi.array().items(require("./packageObjectShort.js").test).required(), 10 | }; 11 | -------------------------------------------------------------------------------- /tests/models/userObjectPrivate.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | schema: { 3 | description: "Privately returned information of users on Pulsar.", 4 | type: "object", 5 | required: ["username", "avatar", "data", "created_at", "packages"], 6 | properties: { 7 | username: { 8 | type: "string", 9 | }, 10 | avatar: { 11 | type: "string", 12 | }, 13 | data: { 14 | type: "object", 15 | }, 16 | node_id: { 17 | type: "string", 18 | }, 19 | token: { 20 | type: "string", 21 | }, 22 | created_at: { 23 | type: "string", 24 | }, 25 | packages: { 26 | type: "array", 27 | }, 28 | }, 29 | }, 30 | example: { 31 | username: "confused-Techie", 32 | avatar: "https://avatar.url", 33 | data: {}, 34 | node_id: "users-node-id", 35 | token: "user-api-token", 36 | created_at: "2023-09-16T00:58:36.755Z", 37 | packages: [], 38 | }, 39 | test: Joi.object({ 40 | username: Joi.string().required(), 41 | avatar: Joi.string().required(), 42 | data: Joi.object().required(), 43 | node_id: Joi.string().required(), 44 | token: Joi.string().required(), 45 | created_at: Joi.string().required(), 46 | packages: Joi.array().required(), 47 | }), 48 | }; 49 | -------------------------------------------------------------------------------- /tests/models/userObjectPublic.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | schema: { 3 | description: "Publically returned information of users on Pulsar.", 4 | type: "object", 5 | required: ["username", "avatar", "data", "created_at", "packages"], 6 | properties: { 7 | username: { 8 | type: "string", 9 | }, 10 | avatar: { 11 | type: "string", 12 | }, 13 | data: { 14 | type: "object", 15 | }, 16 | created_at: { 17 | type: "string", 18 | }, 19 | packages: { 20 | type: "array", 21 | }, 22 | }, 23 | }, 24 | example: { 25 | username: "confused-Techie", 26 | avatar: "https://avatar.url", 27 | data: {}, 28 | created_at: "2023-09-16T00:58:36.755Z", 29 | packages: [], 30 | }, 31 | test: Joi.object({ 32 | username: Joi.string().required(), 33 | avatar: Joi.string().required(), 34 | data: Joi.object().required(), 35 | created_at: Joi.string().required(), 36 | packages: Joi.array().required(), 37 | }), 38 | }; 39 | -------------------------------------------------------------------------------- /tests/models/userObjectPublicArray.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | schema: { 3 | type: "array", 4 | items: { 5 | $ref: "#/components/schemas/userObjectPublic", 6 | }, 7 | }, 8 | example: [require("./userObjectPublic.js").example], 9 | test: Joi.array().items(require("./userObjectPublic.js").test).required(), 10 | }; 11 | -------------------------------------------------------------------------------- /tests/unit/ServerStatusObject.test.js: -------------------------------------------------------------------------------- 1 | const ServerStatus = require("../../src/ServerStatusObject.js"); 2 | const Joi = require("joi"); 3 | 4 | describe("Building Objects with ServerStatus Return as Expected", () => { 5 | test("Formal usage", () => { 6 | const obj = new ServerStatus().isOk().setContent("Hello World").build(); 7 | 8 | const schema = Joi.object() 9 | .keys({ 10 | ok: Joi.boolean().required(), 11 | content: Joi.string().required(), 12 | }) 13 | .required(); 14 | 15 | expect(obj).toMatchSchema(schema); 16 | expect(obj.content).toBe("Hello World"); 17 | expect(obj.ok).toBe(true); 18 | }); 19 | 20 | test("Not OK Formal Usage", () => { 21 | const obj = new ServerStatus() 22 | .notOk() 23 | .setContent("Hello World") 24 | .setShort("We had an error") 25 | .build(); 26 | 27 | const schema = Joi.object() 28 | .keys({ 29 | ok: Joi.boolean().required(), 30 | content: Joi.string().required(), 31 | short: Joi.string().required(), 32 | }) 33 | .required(); 34 | 35 | expect(obj).toMatchSchema(schema); 36 | expect(obj.ok).toBe(false); 37 | expect(obj.short).toBe("We had an error"); 38 | expect(obj.content).toBe("Hello World"); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/unit/cache.test.js: -------------------------------------------------------------------------------- 1 | const cache = require("../../src/cache.js"); 2 | const Joi = require("joi"); 3 | 4 | test("Cache Creates Object As Expected", async () => { 5 | let newCache = new cache.CacheObject("test-contents"); 6 | expect(typeof newCache === "object").toBeTruthy(); 7 | }); 8 | 9 | describe("Cache Objects Have the Functions and Variables Expected", () => { 10 | let newCache = new cache.CacheObject("test-contents", "test-name"); 11 | 12 | test("Cache Object Contains Object Values Expected", async () => { 13 | const schema = Joi.object() 14 | .keys({ 15 | birth: Joi.number().integer().required(), 16 | data: Joi.string().required(), 17 | invalidated: Joi.boolean().required(), 18 | last_validate: Joi.number().integer().required(), 19 | cache_time: Joi.number().integer().required(), 20 | name: Joi.string().required(), 21 | }) 22 | .required(); 23 | 24 | expect(newCache).toMatchSchema(schema); 25 | }); 26 | 27 | test("Cache Object Contains Contents as Instantiated", async () => { 28 | expect(newCache.data).toEqual("test-contents"); 29 | }); 30 | 31 | test("Cache Object Data is not Invalidated Via Variable", async () => { 32 | expect(newCache.invalidated).toEqual(false); 33 | }); 34 | 35 | test("Cache Object contains Name as Instantiated", async () => { 36 | expect(newCache.name).toEqual("test-name"); 37 | }); 38 | 39 | test("Cache Object Contains Function 'invalidate'", async () => { 40 | expect(typeof newCache.invalidate === "function").toBeTruthy(); 41 | }); 42 | 43 | test("Cache Object Returns Boolean for Expired", async () => { 44 | let exp = newCache.Expired; 45 | expect(typeof exp === "boolean").toBeTruthy(); 46 | }); 47 | 48 | test("Cache Object Changes variable when calling 'invalidate'", async () => { 49 | newCache.invalidate(); 50 | expect(newCache.invalidated).toBeTruthy(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/unit/config.test.js: -------------------------------------------------------------------------------- 1 | const config = require("../../src/config.js"); 2 | const Joi = require("joi"); 3 | 4 | describe("Config Returns all Expected Values", () => { 5 | let con = config.getConfig(); 6 | 7 | test("Contains all required keys", async () => { 8 | const schema = Joi.object() 9 | .keys({ 10 | port: Joi.number().integer().required(), 11 | server_url: Joi.string().allow("").required(), 12 | paginated_amount: Joi.number().integer().required(), 13 | prod: Joi.boolean().required(), 14 | cache_time: Joi.number().integer().required(), 15 | GCLOUD_STORAGE_BUCKET: Joi.string().allow("").required(), 16 | GOOGLE_APPLICATION_CREDENTIALS: Joi.string().allow("").required(), 17 | GH_CLIENTID: Joi.string().allow("").required(), 18 | GH_USERAGENT: Joi.string().allow("").required(), 19 | GH_REDIRECTURI: Joi.string().allow("").required(), 20 | GH_CLIENTSECRET: Joi.string().allow("").required(), 21 | DB_HOST: Joi.string().allow("").required(), 22 | DB_USER: Joi.string().allow("").required(), 23 | DB_PASS: Joi.string().allow("").required(), 24 | DB_DB: Joi.string().allow("").required(), 25 | DB_PORT: Joi.number().integer().required(), 26 | DB_SSL_CERT: Joi.string().allow("").required(), 27 | LOG_LEVEL: Joi.number().integer().required(), 28 | LOG_FORMAT: Joi.string().allow("").required(), 29 | RATE_LIMIT_GENERIC: Joi.number().integer().required(), 30 | RATE_LIMIT_AUTH: Joi.number().integer().required(), 31 | WEBHOOK_PUBLISH: Joi.string().allow("").required(), 32 | WEBHOOK_VERSION: Joi.string().allow("").required(), 33 | WEBHOOK_USERNAME: Joi.string().allow("").required(), 34 | }) 35 | .required(); 36 | 37 | expect(con).toMatchSchema(schema); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/unit/controllers/deletePackagesPackageName.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../../src/controllers/deletePackagesPackageName.js"); 2 | 3 | describe("Has features expected", () => { 4 | test("endpoint features", () => { 5 | const expected = { 6 | method: "DELETE", 7 | paths: ["/api/packages/:packageName", "/api/themes/:packageName"], 8 | rateLimit: "auth", 9 | successStatus: 204, 10 | }; 11 | 12 | expect(endpoint.endpoint).toMatchObject(expected); 13 | }); 14 | 15 | test("correct functions", () => { 16 | expect(endpoint.logic).toBeTypeof("function"); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/unit/controllers/deletePackagesPackageNameStar.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../../src/controllers/deletePackagesPackageStar.js"); 2 | 3 | describe("Has features expected", () => { 4 | test("endpoint features", () => { 5 | const expected = { 6 | method: "DELETE", 7 | paths: [ 8 | "/api/packages/:packageName/star", 9 | "/api/themes/:packageName/star", 10 | ], 11 | rateLimit: "auth", 12 | successStatus: 204, 13 | }; 14 | 15 | expect(endpoint.endpoint).toMatchObject(expected); 16 | }); 17 | 18 | test("correct functions", () => { 19 | expect(endpoint.logic).toBeTypeof("function"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/unit/controllers/deletePackagesPackageNameVersionsVersionName.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../../src/controllers/deletePackagesPackageNameVersionsVersionName.js"); 2 | 3 | describe("Has features expected", () => { 4 | test("endpoint features", () => { 5 | const expected = { 6 | method: "DELETE", 7 | paths: [ 8 | "/api/packages/:packageName/versions/:versionName", 9 | "/api/themes/:packageName/versions/:versionName", 10 | ], 11 | rateLimit: "auth", 12 | successStatus: 204, 13 | }; 14 | 15 | expect(endpoint.endpoint).toMatchObject(expected); 16 | }); 17 | 18 | test("correct functions", () => { 19 | expect(endpoint.logic).toBeTypeof("function"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/unit/controllers/getRoot.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../../src/controllers/getRoot.js"); 2 | 3 | describe("Has features expected", () => { 4 | test("Has correct endpoint features", () => { 5 | const expected = { 6 | method: "GET", 7 | paths: ["/"], 8 | rateLimit: "generic", 9 | successStatus: 200, 10 | }; 11 | 12 | expect(endpoint.endpoint).toMatchObject(expected); 13 | }); 14 | 15 | test("Has correct functions", () => { 16 | expect(endpoint.logic).toBeTypeof("function"); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/unit/controllers/getThemes.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../../src/controllers/getThemes.js"); 2 | const context = require("../../../src/context.js"); 3 | 4 | describe("Has features expected", () => { 5 | test("Has correct endpoint features", () => { 6 | const expected = { 7 | method: "GET", 8 | paths: ["/api/themes"], 9 | rateLimit: "generic", 10 | successStatus: 200, 11 | }; 12 | 13 | expect(endpoint.endpoint).toMatchObject(expected); 14 | }); 15 | 16 | test("Has correct functions", () => { 17 | expect(endpoint.logic).toBeTypeof("function"); 18 | }); 19 | }); 20 | 21 | describe("Parameters behave as expected", () => { 22 | test("Returns valid 'page'", () => { 23 | const req = { 24 | query: { 25 | page: "1", 26 | }, 27 | }; 28 | 29 | const res = endpoint.params.page(context, req); 30 | expect(res).toBe(1); 31 | }); 32 | test("Returns valid 'sort'", () => { 33 | const req = { 34 | query: { 35 | sort: "downloads", 36 | }, 37 | }; 38 | 39 | const res = endpoint.params.sort(context, req); 40 | expect(res).toBe("downloads"); 41 | }); 42 | test("Returns valid 'direction'", () => { 43 | const req = { 44 | query: { 45 | direction: "desc", 46 | }, 47 | }; 48 | 49 | const res = endpoint.params.direction(context, req); 50 | expect(res).toBe("desc"); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/unit/controllers/getThemesSearch.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../../src/controllers/getThemesSearch.js"); 2 | const context = require("../../../src/context.js"); 3 | 4 | describe("Has features expected", () => { 5 | test("Has correct endpoint features", () => { 6 | const expected = { 7 | method: "GET", 8 | paths: ["/api/themes/search"], 9 | rateLimit: "generic", 10 | successStatus: 200, 11 | }; 12 | 13 | expect(endpoint.endpoint).toMatchObject(expected); 14 | }); 15 | 16 | test("Has correct functions", () => { 17 | expect(endpoint.logic).toBeTypeof("function"); 18 | }); 19 | }); 20 | 21 | describe("Parameters behave as expected", () => { 22 | test("Returns valid 'sort'", () => { 23 | const req = { 24 | query: { sort: "downloads" }, 25 | }; 26 | 27 | const res = endpoint.params.sort(context, req); 28 | expect(res).toBe("downloads"); 29 | }); 30 | test("Returns valid 'page'", () => { 31 | const req = { 32 | query: { page: "1" }, 33 | }; 34 | 35 | const res = endpoint.params.page(context, req); 36 | expect(res).toBe(1); 37 | }); 38 | test("Returns valid 'direction'", () => { 39 | const req = { 40 | query: { direction: "desc" }, 41 | }; 42 | 43 | const res = endpoint.params.direction(context, req); 44 | expect(res).toBe("desc"); 45 | }); 46 | test("Returns valid 'query'", () => { 47 | const req = { 48 | query: { q: "hello" }, 49 | }; 50 | 51 | const res = endpoint.params.query(context, req); 52 | expect(res).toBe("hello"); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/unit/controllers/getUpdates.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../../src/controllers/getUpdates.js"); 2 | 3 | describe("Has features expected", () => { 4 | test("Has correct endpoint features", () => { 5 | const expected = { 6 | method: "GET", 7 | rateLimit: "generic", 8 | successStatus: 200, 9 | paths: ["/api/updates"], 10 | }; 11 | 12 | expect(endpoint.endpoint).toMatchObject(expected); 13 | }); 14 | 15 | test("Has correct functions", () => { 16 | expect(endpoint.logic).toBeTypeof("function"); 17 | }); 18 | }); 19 | 20 | describe("Functions as expected", () => { 21 | test("Returns correct SSO Object", async () => { 22 | const sso = await endpoint.logic({}, require("../../../src/context.js")); 23 | 24 | expect(sso.ok).toBe(false); 25 | expect(sso.short).toBe("not_supported"); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/unit/controllers/getUsers.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../../src/controllers/getUsers.js"); 2 | const context = require("../../../src/context.js"); 3 | 4 | describe("Has features expected", () => { 5 | test("Has correct endpoint features", () => { 6 | const expected = { 7 | method: "GET", 8 | paths: ["/api/users"], 9 | rateLimit: "auth", 10 | successStatus: 200, 11 | }; 12 | 13 | expect(endpoint.endpoint).toMatchObject(expected); 14 | }); 15 | 16 | test("Has correct functions", () => { 17 | expect(endpoint.logic).toBeTypeof("function"); 18 | expect(endpoint.preLogic).toBeTypeof("function"); 19 | expect(endpoint.postLogic).toBeTypeof("function"); 20 | }); 21 | }); 22 | 23 | describe("Parameters function as expected", () => { 24 | test("Returns params as provided", () => { 25 | const req = { 26 | get: (wants) => { 27 | if (wants === "Authorization") { 28 | return "Auth-Token"; 29 | } else { 30 | return ""; 31 | } 32 | }, 33 | }; 34 | 35 | const res = endpoint.params.auth(context, req); 36 | 37 | expect(res).toBe("Auth-Token"); 38 | }); 39 | 40 | test("Returns params when missing", () => { 41 | const req = { 42 | get: () => { 43 | return ""; 44 | }, 45 | }; 46 | 47 | const res = endpoint.params.auth(context, req); 48 | 49 | expect(res).toBe(""); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/unit/controllers/getUsersLogin.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../../src/controllers/getUsersLogin.js"); 2 | const context = require("../../../src/context.js"); 3 | 4 | describe("Has features expected", () => { 5 | test("Has correct endpoint features", () => { 6 | const expected = { 7 | method: "GET", 8 | paths: ["/api/users/:login"], 9 | rateLimit: "generic", 10 | successStatus: 200, 11 | }; 12 | 13 | expect(endpoint.endpoint).toMatchObject(expected); 14 | }); 15 | }); 16 | 17 | describe("Parameters function as expected", () => { 18 | test("Returns params as provided", () => { 19 | const req = { 20 | params: { 21 | login: "test-user", 22 | }, 23 | }; 24 | 25 | const res = endpoint.params.login(context, req); 26 | 27 | expect(res).toBe("test-user"); 28 | }); 29 | 30 | test("Returns params when missing", () => { 31 | const req = { params: {} }; 32 | 33 | const res = endpoint.params.login(context, req); 34 | 35 | expect(res).toBe(""); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/unit/controllers/postPackagesPackageNameVersions.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../../src/controllers/postPackagesPackageNameVersions.js"); 2 | 3 | describe("Has features expected", () => { 4 | test("Has correct endpoint features", () => { 5 | const expected = { 6 | method: "POST", 7 | rateLimit: "auth", 8 | successStatus: 201, 9 | }; 10 | 11 | expect(endpoint.endpoint).toMatchObject(expected); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/unit/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.test.js: -------------------------------------------------------------------------------- 1 | const endpoint = require("../../../src/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js"); 2 | const context = require("../../../src/context.js"); 3 | 4 | describe("Has features expected", () => { 5 | test("Has correct endpoint features", () => { 6 | const expected = { 7 | method: "POST", 8 | rateLimit: "auth", 9 | successStatus: 201, 10 | }; 11 | 12 | expect(endpoint.endpoint).toMatchObject(expected); 13 | }); 14 | }); 15 | 16 | describe("Returns as expected", () => { 17 | test("Returns simple OK object", async () => { 18 | const res = await endpoint.logic({}, context); 19 | 20 | expect(res.ok).toBe(true); 21 | expect(res.content).toBeDefined(); 22 | expect(res.content.ok).toBe(true); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/unit/endpoints.test.js: -------------------------------------------------------------------------------- 1 | const endpoints = require("../../src/controllers/endpoints.js"); 2 | 3 | describe("All endpoints are valid", () => { 4 | test("Have expected objects", () => { 5 | for (const node of endpoints) { 6 | for (const item in node) { 7 | const validItems = [ 8 | "docs", 9 | "endpoint", 10 | "params", 11 | "logic", 12 | "preLogic", 13 | "postLogic", 14 | "postReturnHTTP", 15 | ]; 16 | 17 | expect(validItems.includes(item)); 18 | } 19 | } 20 | }); 21 | 22 | test("Have a valid 'endpoint' object", () => { 23 | for (const node of endpoints) { 24 | const endpoint = node.endpoint; 25 | 26 | expect(endpoint.method).toBeTypeof("string"); 27 | expect(endpoint.method).toBeIncludedBy(["GET", "POST", "DELETE"]); 28 | expect(endpoint.paths).toBeArray(); 29 | expect(endpoint.rateLimit).toBeTypeof("string"); 30 | expect(endpoint.rateLimit).toBeIncludedBy(["generic", "auth"]); 31 | expect(endpoint.successStatus).toBeTypeof("number"); 32 | expect(endpoint.options).toBeDefined(); 33 | 34 | if (endpoint.endpointKind) { 35 | expect(endpoint.endpointKind).toBeTypeof("string"); 36 | expect(endpoint.endpointKind).toBeIncludedBy(["raw", "default"]); 37 | } 38 | } 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/unit/models/constructPackageObjectFull.test.js: -------------------------------------------------------------------------------- 1 | const pof = require("../../../src/models/constructPackageObjectFull.js"); 2 | const schema = require("../../models/packageObjectFull.js").test; 3 | 4 | describe("Parses Data, as expected to be returned by the Database", () => { 5 | test("Correctly Parses normal data", async () => { 6 | const data = { 7 | pointer: "1234", 8 | name: "test-package", 9 | created: "2024-01-20T00:47:00.981Z", 10 | updated: "2024-01-20T00:47:00.981Z", 11 | creation_method: "Test Package", 12 | downloads: "25", 13 | data: { 14 | name: "test-package", 15 | owner: "pulsar-edit", 16 | readme: "This is a readme!", 17 | metadata: { 18 | name: "test-package", 19 | license: "MIT", 20 | version: "1.0.0", 21 | }, 22 | releases: { latest: "1.0.0" }, 23 | versions: { 24 | "1.0.0": { 25 | sha: "1234", 26 | name: "test-package", 27 | version: "1.0.0", 28 | tarball_url: "https://nowhere.com", 29 | }, 30 | }, 31 | repository: { 32 | url: "https://github.com/pulsar-edit/test-package", 33 | type: "git", 34 | }, 35 | creation_method: "Test Package", 36 | }, 37 | owner: "pulsar-edit", 38 | stargazers_count: "1", 39 | versions: [ 40 | { 41 | id: 10, 42 | meta: { 43 | sha: "1234", 44 | name: "test-package", 45 | version: "1.0.0", 46 | tarball_url: "https://nowhere.com", 47 | }, 48 | engine: { atom: "*" }, 49 | semver: "1.0.0", 50 | license: "MIT", 51 | package: "1234", 52 | hasGrammar: false, 53 | hasSnippets: false, 54 | supportedLanguages: null, 55 | }, 56 | ], 57 | }; 58 | 59 | const parsed = await pof(data); 60 | 61 | expect(parsed).toMatchSchema(schema); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /tests/unit/models/constructPackageObjectJSON.test.js: -------------------------------------------------------------------------------- 1 | const poj = require("../../../src/models/constructPackageObjectJSON.js"); 2 | const schema = require("../../models/packageObjectJSON.js").test; 3 | 4 | describe("Parses Data, as expected to be returned by the Database", () => { 5 | test("Correctly Parses normal data", async () => { 6 | const data = { 7 | semver: "1.0.0", 8 | license: "MIT", 9 | engines: { atom: "*" }, 10 | meta: { 11 | sha: "1234", 12 | name: "package-test", 13 | version: "1.0.0", 14 | tarball_url: "https://nowhere.com", 15 | }, 16 | }; 17 | 18 | const parsed = await poj(data); 19 | 20 | expect(parsed).toMatchSchema(schema); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/unit/models/constructPackageObjectShort.test.js: -------------------------------------------------------------------------------- 1 | const pos = require("../../../src/models/constructPackageObjectShort.js"); 2 | const schema = require("../../models/packageObjectShort.js").test; 3 | 4 | describe("Parses Data, as expected to be returned by the Database", () => { 5 | test("Correctly Parses normal data", async () => { 6 | const data = { 7 | name: "test-package", 8 | data: { 9 | name: "test-package", 10 | owner: "pulsar-edit", 11 | readme: "This is a readme!", 12 | metadata: { 13 | name: "test-package", 14 | license: "MIT", 15 | version: "1.0.0", 16 | }, 17 | releases: { latest: "1.0.0" }, 18 | versions: { 19 | "1.0.0": { 20 | sha: "1234", 21 | name: "test-package", 22 | version: "1.0.0", 23 | tarball_url: "https://nowhere.com", 24 | }, 25 | }, 26 | repository: { 27 | url: "https://github.com/pulsar-edit/test-package", 28 | type: "git", 29 | }, 30 | creation_method: "Test Package", 31 | }, 32 | downloads: "0", 33 | owner: "pulsar-edit", 34 | stargazers_count: "0", 35 | semver: "1.0.0", 36 | created: "2024-01-20T00:46:57.014Z", 37 | updated: "2024-01-20T00:46:57.014Z", 38 | creation_method: "Test Package", 39 | }; 40 | 41 | const parsed = await pos(data); 42 | 43 | expect(parsed).toMatchSchema(schema); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/unit/storage.test.js: -------------------------------------------------------------------------------- 1 | const storage = require("../../src/storage.js"); 2 | 3 | describe("Functions Return Proper Values", () => { 4 | test("getBanList Returns Array", async () => { 5 | let value = await storage.getBanList(); 6 | expect(value.content).toBeArray(); 7 | }); 8 | 9 | test("getFeaturedPackages Returns Array", async () => { 10 | let value = await storage.getFeaturedPackages(); 11 | expect(value.content).toBeArray(); 12 | }); 13 | 14 | test("getFeaturedThemes Returns Array", async () => { 15 | let value = await storage.getFeaturedThemes(); 16 | expect(value.content).toBeArray(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/vcs/vcs.vcs.test.js: -------------------------------------------------------------------------------- 1 | const httpMock = require("../helpers/httpMock.helper.jest.js"); 2 | 3 | const vcs = require("../../src/vcs.js"); 4 | 5 | let http_cache = { 6 | pack1: {}, // pack1 will be used for newPackageData tests 7 | pack2: {}, // pack2 will be used for newVersionData tests 8 | }; 9 | 10 | const userDataGeneric = { 11 | token: "123", 12 | node_id: "456", 13 | }; 14 | 15 | describe("Does NewPackageData Return as expected", () => { 16 | test("Repo Exists Error on Bad WebRequest", async () => { 17 | const ownerRepo = "confused-Techie/pulsar-backend"; 18 | 19 | http_cache.pack1.bad_exists = new httpMock.HTTP(`/repos/${ownerRepo}`) 20 | .ok(false) 21 | .short("Failed Request") 22 | .status(404) 23 | .parse(); 24 | 25 | const tmpMock = httpMock.webRequestMock([http_cache.pack1.bad_exists]); 26 | 27 | const res = await vcs.newPackageData(userDataGeneric, ownerRepo, "git"); 28 | 29 | expect(res.ok).toBe(false); 30 | expect(res.short).toBe("Bad Repo"); 31 | expect(res.content).toBe(`Failed to get repo: ${ownerRepo} - Bad Repo`); 32 | }); 33 | }); 34 | 35 | describe("Ownership Returns as Expected", () => { 36 | test("Returns Successfully", async () => { 37 | const ownerRepo = "pulsar-edit/pulsar"; 38 | const userObj = { 39 | token: "123", 40 | node_id: "12345", 41 | }; 42 | const packObj = { 43 | repository: { 44 | type: "git", 45 | url: "https://github.com/pulsar-edit/pulsar", 46 | }, 47 | data: {}, 48 | }; 49 | 50 | const mockData = new httpMock.HTTP( 51 | `/repos/${ownerRepo}/collaborators?page=1` 52 | ) 53 | .ok(true) 54 | .status(200) 55 | .body([ 56 | { 57 | login: "confused-Techie", 58 | node_id: userObj.node_id, 59 | permissions: { 60 | admin: true, 61 | maintain: true, 62 | push: true, 63 | triage: true, 64 | pull: true, 65 | }, 66 | role_name: "admin", 67 | }, 68 | ]) 69 | .parse(); 70 | 71 | const tmpMock = httpMock.webRequestMock([mockData]); 72 | 73 | const res = await vcs.ownership(userObj, packObj); 74 | 75 | expect(res.ok).toBe(true); 76 | expect(res.content).toBe("admin"); 77 | }); 78 | }); 79 | --------------------------------------------------------------------------------