diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 453151ed..18ff214e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,47 +1,46 @@ name: CI -on: [push, pull_request] +on: push jobs: - unit-test: + unit-test: runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18.x] name: Unit Tests steps: - uses: actions/checkout@v3 - - name: set node.js ${{ matrix.node-version }} + - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node-version }} + node-version: "22.x" - name: npm ci run: npm ci - name: Unit tests run: npm run test:unit e2e-test: - runs-on: ubuntu-latest - container: ghcr.io/openupm/openupm-cli-e2e-${{ matrix.os }}:latest + runs-on: ${{ matrix.os }}-latest strategy: matrix: os: - ubuntu - # Seems like windows is not supported at the moment (06.2024) - # - windows + - windows + node: + - "18.x" # Oldest supported lts + - "20.x" # Newest lts + - "22.x" # Latest name: E2E Tests steps: - uses: actions/checkout@v3 - name: setup node.js uses: actions/setup-node@v3 with: - node-version: 18 + node-version: ${{ matrix.node }} - name: install deps run: npm ci - name: build run: npm run build - name: run tests - run: npx jest --testMatch "**/*.e2e.ts" --runInBand --silent=false + run: npm run test:e2e release: runs-on: ubuntu-latest diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 9d9323cc..ec8fd912 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,7 @@ { "recommendations": [ "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint" + "dbaeumer.vscode-eslint", + "orta.vscode-jest" ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index fb5147ac..6cab8326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [4.1.1](https://github.com/openupm/openupm-cli/compare/4.1.0...4.1.1) (2024-08-31) + + +### Bug Fixes + +* incorrect import path ([44fe7ea](https://github.com/openupm/openupm-cli/commit/44fe7ea0e5400a5d7403a56bf632b173b8245beb)) + # [4.1.0](https://github.com/openupm/openupm-cli/compare/4.0.0...4.1.0) (2024-08-13) diff --git a/jest.config.js b/jest.config.js index b113f698..a3e3ad37 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,7 +4,7 @@ module.exports = { verbose: true, clearMocks: true, setupFilesAfterEnv: [ - "./test/domain/project-manifest-assertions.ts", + "./test/project-manifest-assertions.ts", "./test/result-assertions.ts", ], }; diff --git a/package-lock.json b/package-lock.json index 5d6d5326..156d0633 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openupm-cli", - "version": "4.1.0", + "version": "4.1.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "openupm-cli", - "version": "4.1.0", + "version": "4.1.1", "license": "BSD-3-Clause", "dependencies": { "@commander-js/extra-typings": "^9.5.0", diff --git a/package.json b/package.json index 364c6a0f..25d7a8b5 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "openupm-cli", - "version": "4.1.0", + "version": "4.1.1", "preferGlobal": true, "description": "openupm command line interface", "engines": { "node": ">=18" }, "scripts": { - "test:unit": "cross-env NODE_ENV=test jest --testMatch \"**/*.test.ts\"", + "test:e2e": "jest --testMatch \"**/e2e/**/*.test.ts\" --runInBand", + "test:unit": "cross-env NODE_ENV=test jest --testMatch \"**/unit/**/*.test.ts\"", "clean": "rimraf lib", "build": "npm run clean && tsc -p tsconfig.build.json", "build:watch": "tsc -w -p tsconfig.build.json", diff --git a/src/cli/cmd-add.ts b/src/cli/cmd-add.ts index 76e4dfb5..efcc5a94 100644 --- a/src/cli/cmd-add.ts +++ b/src/cli/cmd-add.ts @@ -42,6 +42,8 @@ import { CmdOptions } from "./options"; import { ResultCodes } from "./result-codes"; import { ResolvePackumentVersionError } from "../domain/packument"; +import { unityRegistry } from "../domain/registry"; +import { GetRegistryAuth } from "../services/get-registry-auth"; import { GetRegistryPackumentVersion } from "../services/get-registry-packument-version"; import { isZod } from "../utils/zod-utils"; @@ -124,6 +126,7 @@ export function makeAddCmd( loadProjectManifest: LoadProjectManifest, saveProjectManifest: SaveProjectManifest, determineEditorVersion: DetermineEditorVersion, + getRegistryAuth: GetRegistryAuth, log: Logger, debugLog: DebugLog ): AddCmd { @@ -141,6 +144,11 @@ export function makeAddCmd( `${editorVersion} is unknown, the editor version check is disabled` ); + const primaryRegistry = await getRegistryAuth( + env.systemUser, + env.primaryRegistryUrl + ); + const tryAddToManifest = async function ( manifest: UnityProjectManifest, pkg: PackageReference @@ -160,13 +168,13 @@ export function makeAddCmd( let resolveResult = await getRegistryPackumentVersion( name, requestedVersion, - env.registry + primaryRegistry ).promise; if (resolveResult.isErr() && env.upstream) { const upstreamResult = await getRegistryPackumentVersion( name, requestedVersion, - env.upstreamRegistry + unityRegistry ).promise; if (upstreamResult.isOk()) { resolveResult = upstreamResult; @@ -213,7 +221,7 @@ export function makeAddCmd( if (!isUpstreamPackage) { debugLog(`fetch: ${makePackageReference(name, requestedVersion)}`); const dependencyGraph = await resolveDependencies( - [env.registry, env.upstreamRegistry], + [primaryRegistry, unityRegistry], name, versionToAdd, true @@ -295,15 +303,20 @@ export function makeAddCmd( } if (!isUpstreamPackage && pkgsInScope.length > 0) { - manifest = mapScopedRegistry(manifest, env.registry.url, (initial) => { - let updated = initial ?? makeEmptyScopedRegistryFor(env.registry.url); - - updated = pkgsInScope.reduce(addScope, updated!); - dirty = - !areArraysEqual(updated!.scopes, initial?.scopes ?? []) || dirty; - - return updated; - }); + manifest = mapScopedRegistry( + manifest, + primaryRegistry.url, + (initial) => { + let updated = + initial ?? makeEmptyScopedRegistryFor(primaryRegistry.url); + + updated = pkgsInScope.reduce(addScope, updated!); + dirty = + !areArraysEqual(updated!.scopes, initial?.scopes ?? []) || dirty; + + return updated; + } + ); } if (options.test) manifest = addTestable(manifest, name); diff --git a/src/cli/cmd-deps.ts b/src/cli/cmd-deps.ts index 8db6c890..b55beafa 100644 --- a/src/cli/cmd-deps.ts +++ b/src/cli/cmd-deps.ts @@ -1,23 +1,25 @@ -import { ParseEnv } from "../services/parse-env"; -import { PackageUrl } from "../domain/package-url"; +import chalk from "chalk"; +import { Logger } from "npmlog"; +import os from "os"; +import { PackumentNotFoundError } from "../common-errors"; import { makePackageReference, PackageReference, splitPackageReference, } from "../domain/package-reference"; -import { CmdOptions } from "./options"; -import { PackumentNotFoundError } from "../common-errors"; -import { ResolveDependencies } from "../services/dependency-resolving"; -import { Logger } from "npmlog"; +import { PackageUrl } from "../domain/package-url"; +import { unityRegistry } from "../domain/registry"; +import { SemanticVersion } from "../domain/semantic-version"; import { DebugLog } from "../logging"; -import { ResultCodes } from "./result-codes"; +import { ResolveDependencies } from "../services/dependency-resolving"; import { GetLatestVersion } from "../services/get-latest-version"; -import { SemanticVersion } from "../domain/semantic-version"; +import { GetRegistryAuth } from "../services/get-registry-auth"; +import { ParseEnv } from "../services/parse-env"; +import { queryAllRegistriesLazy } from "../utils/sources"; import { isZod } from "../utils/zod-utils"; import { stringifyDependencyGraph } from "./dependency-logging"; -import os from "os"; -import chalk from "chalk"; -import { queryAllRegistriesLazy } from "../utils/sources"; +import { CmdOptions } from "./options"; +import { ResultCodes } from "./result-codes"; /** * Options passed to the deps command. @@ -51,13 +53,18 @@ export function makeDepsCmd( parseEnv: ParseEnv, resolveDependencies: ResolveDependencies, resolveLatestVersion: GetLatestVersion, + getRegistryAuth: GetRegistryAuth, log: Logger, debugLog: DebugLog ): DepsCmd { return async (pkg, options) => { // parse env const env = await parseEnv(options); - const sources = [env.registry, env.upstreamRegistry]; + const primaryRegistry = await getRegistryAuth( + env.systemUser, + env.primaryRegistryUrl + ); + const sources = [primaryRegistry, unityRegistry]; const [packageName, requestedVersion] = splitPackageReference(pkg); diff --git a/src/cli/cmd-login.ts b/src/cli/cmd-login.ts index 03e06625..9132ac82 100644 --- a/src/cli/cmd-login.ts +++ b/src/cli/cmd-login.ts @@ -76,7 +76,7 @@ export function makeLoginCmd( const alwaysAuth = options.alwaysAuth || false; - const configPath = await getUpmConfigPath(env.systemUser); + const configPath = getUpmConfigPath(env.systemUser); await login( username, diff --git a/src/cli/cmd-search.ts b/src/cli/cmd-search.ts index 6d678aa2..883a11a2 100644 --- a/src/cli/cmd-search.ts +++ b/src/cli/cmd-search.ts @@ -1,10 +1,11 @@ +import { Logger } from "npmlog"; import * as os from "os"; +import { DebugLog } from "../logging"; +import { GetRegistryAuth } from "../services/get-registry-auth"; import { ParseEnv } from "../services/parse-env"; +import { SearchPackages } from "../services/search-packages"; import { CmdOptions } from "./options"; import { formatAsTable } from "./output-formatting"; -import { Logger } from "npmlog"; -import { SearchPackages } from "../services/search-packages"; -import { DebugLog } from "../logging"; import { ResultCodes } from "./result-codes"; /** @@ -33,15 +34,20 @@ export type SearchCmd = ( export function makeSearchCmd( parseEnv: ParseEnv, searchPackages: SearchPackages, + getRegistryAuth: GetRegistryAuth, log: Logger, debugLog: DebugLog ): SearchCmd { return async (keyword, options) => { // parse env const env = await parseEnv(options); + const primaryRegistry = await getRegistryAuth( + env.systemUser, + env.primaryRegistryUrl + ); let usedEndpoint = "npmsearch"; - const results = await searchPackages(env.registry, keyword, () => { + const results = await searchPackages(primaryRegistry, keyword, () => { usedEndpoint = "endpoint.all"; log.warn("", "fast search endpoint is not available, using old search."); }); @@ -52,7 +58,7 @@ export function makeSearchCmd( } debugLog(`${usedEndpoint}: ${results.map((it) => it.name).join(os.EOL)}`); - console.log(formatAsTable(results)); + log.notice("", formatAsTable(results)); return ResultCodes.Ok; }; } diff --git a/src/cli/cmd-view.ts b/src/cli/cmd-view.ts index c262bee7..d8938768 100644 --- a/src/cli/cmd-view.ts +++ b/src/cli/cmd-view.ts @@ -1,19 +1,19 @@ -import chalk from "chalk"; -import assert from "assert"; -import { tryGetLatestVersion, UnityPackument } from "../domain/packument"; -import { ParseEnv } from "../services/parse-env"; +import { Logger } from "npmlog"; +import { EOL } from "os"; +import { PackumentNotFoundError } from "../common-errors"; import { hasVersion, PackageReference, splitPackageReference, } from "../domain/package-reference"; -import { CmdOptions } from "./options"; -import { recordKeys } from "../utils/record-utils"; -import { Logger } from "npmlog"; -import { ResultCodes } from "./result-codes"; +import { unityRegistry } from "../domain/registry"; import { GetRegistryPackument } from "../io/packument-io"; +import { GetRegistryAuth } from "../services/get-registry-auth"; +import { ParseEnv } from "../services/parse-env"; import { queryAllRegistriesLazy } from "../utils/sources"; -import { PackumentNotFoundError } from "../common-errors"; +import { CmdOptions } from "./options"; +import { formatPackumentInfo } from "./output-formatting"; +import { ResultCodes } from "./result-codes"; /** * Options passed to the view command. @@ -35,86 +35,22 @@ export type ViewCmd = ( options: ViewOptions ) => Promise; -const printInfo = function (packument: UnityPackument) { - const versionCount = recordKeys(packument.versions).length; - const ver = tryGetLatestVersion(packument); - assert(ver !== undefined); - const verInfo = packument.versions[ver]!; - const license = verInfo.license || "proprietary or unlicensed"; - const displayName = verInfo.displayName; - const description = verInfo.description || packument.description; - const keywords = verInfo.keywords || packument.keywords; - const homepage = verInfo.homepage; - const dist = verInfo.dist; - const dependencies = verInfo.dependencies; - const latest = packument["dist-tags"]?.latest; - let time = packument.time?.modified; - if ( - !time && - latest && - packument.time !== undefined && - latest in packument.time - ) - time = packument.time[latest]; - - console.log(); - console.log( - chalk.greenBright(packument.name) + - "@" + - chalk.greenBright(ver) + - " | " + - chalk.green(license) + - " | versions: " + - chalk.yellow(versionCount) - ); - console.log(); - if (displayName) console.log(chalk.greenBright(displayName)); - if (description) console.log(description); - if (description && description.includes("\n")) console.log(); - if (homepage) console.log(chalk.cyan(homepage)); - if (keywords) console.log(`keywords: ${keywords.join(", ")}`); - - if (dist) { - console.log(); - console.log("dist"); - console.log(".tarball: " + chalk.cyan(dist.tarball)); - if (dist.shasum) console.log(".shasum: " + chalk.yellow(dist.shasum)); - if (dist.integrity) - console.log(".integrity: " + chalk.yellow(dist.integrity)); - } - - if (dependencies && recordKeys(dependencies).length > 0) { - console.log(); - console.log("dependencies"); - recordKeys(dependencies) - .sort() - .forEach((n) => console.log(chalk.yellow(n) + ` ${dependencies[n]}`)); - } - - console.log(); - console.log("latest: " + chalk.greenBright(latest)); - - console.log(); - console.log("published at " + chalk.yellow(time)); - - console.log(); - console.log("versions:"); - for (const version in packument.versions) { - console.log(" " + chalk.greenBright(version)); - } -}; - /** * Makes a {@link ViewCmd} function. */ export function makeViewCmd( parseEnv: ParseEnv, getRegistryPackument: GetRegistryPackument, + getRegistryAuth: GetRegistryAuth, log: Logger ): ViewCmd { return async (pkg, options) => { // parse env const env = await parseEnv(options); + const primaryRegistry = await getRegistryAuth( + env.systemUser, + env.primaryRegistryUrl + ); // parse name if (hasVersion(pkg)) { @@ -124,10 +60,7 @@ export function makeViewCmd( } // verify name - const sources = [ - env.registry, - ...(env.upstream ? [env.upstreamRegistry] : []), - ]; + const sources = [primaryRegistry, ...(env.upstream ? [unityRegistry] : [])]; const packumentFromRegistry = await queryAllRegistriesLazy( sources, (source) => getRegistryPackument(source, pkg) @@ -135,7 +68,8 @@ export function makeViewCmd( const packument = packumentFromRegistry?.value ?? null; if (packument === null) throw new PackumentNotFoundError(pkg); - printInfo(packument); + const output = formatPackumentInfo(packument, EOL); + log.notice("", output); return ResultCodes.Ok; }; } diff --git a/src/cli/error-logging.ts b/src/cli/error-logging.ts index 4bed548e..d603d0b3 100644 --- a/src/cli/error-logging.ts +++ b/src/cli/error-logging.ts @@ -1,25 +1,14 @@ +import { EOL } from "node:os"; import { Logger } from "npmlog"; -import { ResultCodes } from "./result-codes"; -import { RegistryAuthLoadError } from "../services/parse-env"; import { EditorVersionNotSupportedError, PackumentNotFoundError, } from "../common-errors"; import { stringifyEditorVersion } from "../domain/editor-version"; -import { - CompatibilityCheckFailedError, - PackageIncompatibleError, - UnresolvedDependenciesError, -} from "./cmd-add"; import { NoVersionsError, VersionNotFoundError } from "../domain/packument"; -import { EOL } from "node:os"; +import { NoSystemUserProfilePath } from "../domain/upm-config"; import { EditorNotInstalledError } from "../io/builtin-packages"; import { RegistryAuthenticationError } from "../io/common-errors"; -import { - NoHomePathError, - OSNotSupportedError, - VersionNotSupportedOnOsError, -} from "../io/special-paths"; import { ManifestMalformedError, ManifestMissingError, @@ -28,7 +17,18 @@ import { ProjectVersionMalformedError, ProjectVersionMissingError, } from "../io/project-version-io"; -import { NoSystemUserProfilePath } from "../io/upm-config-io"; +import { + NoHomePathError, + OSNotSupportedError, + VersionNotSupportedOnOsError, +} from "../io/special-paths"; +import { RegistryAuthLoadError } from "../services/parse-env"; +import { + CompatibilityCheckFailedError, + PackageIncompatibleError, + UnresolvedDependenciesError, +} from "./cmd-add"; +import { ResultCodes } from "./result-codes"; function makeErrorMessageFor(error: unknown): string { if (error instanceof RegistryAuthLoadError) diff --git a/src/cli/index.ts b/src/cli/index.ts index 8acc6b5f..7733e47b 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -3,23 +3,24 @@ import npmlog from "npmlog"; import pkginfo from "pkginfo"; import updateNotifier from "update-notifier"; import pkg from "../../package.json"; -import { getRegistryPackument } from "../io/packument-io"; +import { getRegistryPackumentUsing } from "../io/packument-io"; import { - loadProjectManifest, + loadProjectManifestUsing, saveProjectManifest, } from "../io/project-manifest-io"; -import { getProcessCwd } from "../io/special-paths"; -import { getUpmConfigPath } from "../io/upm-config-io"; -import { npmDebugLog } from "../logging"; -import { resolveDependencies } from "../services/dependency-resolving"; -import { determineEditorVersion } from "../services/determine-editor-version"; -import { getLatestVersion } from "../services/get-latest-version"; -import { getRegistryAuth } from "../services/get-registry-auth"; -import { getRegistryPackumentVersion } from "../services/get-registry-packument-version"; -import { login } from "../services/login"; +import { makeNpmRegistryClient } from "../io/reg-client"; +import { getHomePathFromEnv } from "../io/special-paths"; +import { getUpmConfigPath as getUpmConfigPathUsing } from "../io/upm-config-io"; +import { DebugLog } from "../logging"; +import { resolveDependencies as resolveDependenciesUsing } from "../services/dependency-resolving"; +import { determineEditorVersionUsing } from "../services/determine-editor-version"; +import { getLatestVersion as getLatestVersionUsing } from "../services/get-latest-version"; +import { getRegistryAuthUsing } from "../services/get-registry-auth"; +import { getRegistryPackumentVersionUsing } from "../services/get-registry-packument-version"; +import { login as loginUsing } from "../services/login"; import { makeParseEnv } from "../services/parse-env"; -import { removePackages } from "../services/remove-packages"; -import { searchPackages } from "../services/search-packages"; +import { removePackages as removePackagesUsing } from "../services/remove-packages"; +import { searchPackagesUsing } from "../services/search-packages"; import { eachValue } from "./cli-parsing"; import { makeAddCmd } from "./cmd-add"; import { makeDepsCmd } from "./cmd-deps"; @@ -39,35 +40,70 @@ import { const log = npmlog; -const parseEnv = makeParseEnv( - log, - getUpmConfigPath, - getRegistryAuth, - getProcessCwd, - npmDebugLog -); +/** + * {@link DebugLog} function which uses {@link npmlog} to print logs to + * the console. + */ +const debugLogToConsole: DebugLog = function (message, context) { + const contextMessage = + context !== undefined + ? `\n${ + context instanceof Error + ? context.toString() + : JSON.stringify(context, null, 2) + }` + : ""; + return log.verbose("", `${message}${contextMessage}`); +}; + +const parseEnv = makeParseEnv(log, process.cwd()); +const homePath = getHomePathFromEnv(process.env); + +const registryClient = makeNpmRegistryClient(log); const addCmd = makeAddCmd( parseEnv, - getRegistryPackumentVersion, - resolveDependencies, - loadProjectManifest, + getRegistryPackumentVersionUsing(registryClient, debugLogToConsole), + resolveDependenciesUsing(registryClient, debugLogToConsole), + loadProjectManifestUsing(debugLogToConsole), saveProjectManifest, - determineEditorVersion, + determineEditorVersionUsing(debugLogToConsole), + getRegistryAuthUsing(homePath, debugLogToConsole), log, - npmDebugLog + debugLogToConsole +); +const loginCmd = makeLoginCmd( + parseEnv, + getUpmConfigPathUsing(homePath), + loginUsing(homePath, registryClient, debugLogToConsole), + log +); +const searchCmd = makeSearchCmd( + parseEnv, + searchPackagesUsing(debugLogToConsole), + getRegistryAuthUsing(homePath, debugLogToConsole), + log, + debugLogToConsole ); -const loginCmd = makeLoginCmd(parseEnv, getUpmConfigPath, login, log); -const searchCmd = makeSearchCmd(parseEnv, searchPackages, log, npmDebugLog); const depsCmd = makeDepsCmd( parseEnv, - resolveDependencies, - getLatestVersion, + resolveDependenciesUsing(registryClient, debugLogToConsole), + getLatestVersionUsing(registryClient, debugLogToConsole), + getRegistryAuthUsing(homePath, debugLogToConsole), log, - npmDebugLog + debugLogToConsole +); +const removeCmd = makeRemoveCmd( + parseEnv, + removePackagesUsing(debugLogToConsole), + log +); +const viewCmd = makeViewCmd( + parseEnv, + getRegistryPackumentUsing(registryClient, debugLogToConsole), + getRegistryAuthUsing(homePath, debugLogToConsole), + log ); -const removeCmd = makeRemoveCmd(parseEnv, removePackages, log); -const viewCmd = makeViewCmd(parseEnv, getRegistryPackument, log); // update-notifier diff --git a/src/cli/output-formatting.ts b/src/cli/output-formatting.ts index ece5ed1c..6f8866dd 100644 --- a/src/cli/output-formatting.ts +++ b/src/cli/output-formatting.ts @@ -1,10 +1,12 @@ +import assert from "assert"; +import chalk from "chalk"; import Table from "cli-table"; import { tryGetLatestVersion, UnityPackument, VersionedPackument, } from "../domain/packument"; -import assert from "assert"; +import { recordKeys } from "../utils/record-utils"; /** * A type describing the minimum required properties of a packument @@ -45,3 +47,89 @@ export function formatAsTable( rows.forEach((row) => table.push(row)); return table.toString(); } + +/** + * Creates a string from a packument containing relevant information. This can + * be printed to the console. + * + * The string is multiline. + * @param packument The packument to format. + * @param eol The string to use to delimit lines, for example {@link import("os").EOL}. + * @returns The formatted string. + */ +export function formatPackumentInfo( + packument: UnityPackument, + eol: string +): string { + let output = ""; + + const versionCount = recordKeys(packument.versions).length; + const ver = tryGetLatestVersion(packument); + assert(ver !== undefined); + const verInfo = packument.versions[ver]!; + const license = verInfo.license || "proprietary or unlicensed"; + const displayName = verInfo.displayName; + const description = verInfo.description || packument.description; + const keywords = verInfo.keywords || packument.keywords; + const homepage = verInfo.homepage; + const dist = verInfo.dist; + const dependencies = verInfo.dependencies; + const latest = packument["dist-tags"]?.latest; + let time = packument.time?.modified; + if ( + !time && + latest && + packument.time !== undefined && + latest in packument.time + ) + time = packument.time[latest]; + + output += eol; + output += `${chalk.greenBright(packument.name)}@${chalk.greenBright( + ver + )} | ${chalk.green(license)} | versions: ${chalk.yellow( + versionCount + )}${eol}${eol}`; + + if (displayName) output += chalk.greenBright(displayName) + eol; + if (description) output += description + eol; + if (description && description.includes("\n")) output += eol; + if (homepage) output += chalk.cyan(homepage) + eol; + if (keywords) output += `keywords: ${keywords.join(", ")}` + eol; + + if (dist) { + output += eol; + output += `dist${eol}`; + output += `.tarball: ${chalk.cyan(dist.tarball)}${eol}`; + if (dist.shasum) output += `.shasum: ${chalk.yellow(dist.shasum)}${eol}`; + if (dist.integrity) + output += `.integrity: ${chalk.yellow(dist.integrity)}${eol}`; + } + + if (dependencies && recordKeys(dependencies).length > 0) { + output += eol; + output += `dependencies${eol}`; + output += recordKeys(dependencies) + .sort() + .reduce( + (acc, dependencyName) => + `${acc + chalk.yellow(dependencyName)} ${ + dependencies[dependencyName] + }${eol}`, + "" + ); + } + + output += eol; + output += `latest: ${chalk.greenBright(latest)}${eol}`; + + output += eol; + output += `published at ${chalk.yellow(time)}${eol}`; + + output += eol; + output += `versions:${eol}${Object.keys(packument.versions) + .map((version) => " " + chalk.greenBright(version)) + .join(eol)}`; + + return output; +} diff --git a/src/domain/package-manifest.ts b/src/domain/package-manifest.ts index fc104096..b92e6e81 100644 --- a/src/domain/package-manifest.ts +++ b/src/domain/package-manifest.ts @@ -91,7 +91,7 @@ export function dependenciesOf( * @param packageManifest The manifest for which to get the editor. * @returns The editor-version or null if the package is compatible * with all Unity version. - * @throws MalformedPackumentError if the packument contains invalid data that + * @throws {MalformedPackumentError} If the packument contains invalid data that * can not be parsed. */ export function tryGetTargetEditorVersionFor( diff --git a/src/domain/packument.ts b/src/domain/packument.ts index 5a33cd49..f87415d7 100644 --- a/src/domain/packument.ts +++ b/src/domain/packument.ts @@ -1,14 +1,13 @@ -import { SemanticVersion } from "./semantic-version"; -import { DomainName } from "./domain-name"; -import { UnityPackageManifest } from "./package-manifest"; -import { PackageId } from "./package-id"; -import { Dist, Maintainer } from "@npm/types"; +import { Dist } from "@npm/types"; import { CustomError } from "ts-custom-error"; import { Err, Ok, Result } from "ts-results-es"; import { recordKeys } from "../utils/record-utils"; +import { DomainName } from "./domain-name"; +import { UnityPackageManifest } from "./package-manifest"; +import { SemanticVersion } from "./semantic-version"; -import { ResolvableVersion } from "./package-reference"; import { PackumentNotFoundError } from "../common-errors"; +import { ResolvableVersion } from "./package-reference"; /** * Contains information about a specific version of a package. This is based on @@ -145,7 +144,7 @@ export type ResolvePackumentVersionError = * @param requestedVersion The version to resolve. In this case indicates that * the latest version is requested. * @returns Result containing the resolved version. Will never be error. - * @throws NoVersionsError if the packument had no versions at all. + * @throws {NoVersionsError} If the packument had no versions at all. */ export function tryResolvePackumentVersion( packument: UnityPackument, @@ -168,7 +167,7 @@ export function tryResolvePackumentVersion( * @param requestedVersion The version to resolve. * @returns A result containing the version or an error if it could not be * resolved. - * @throws NoVersionsError if the packument had no versions at all. + * @throws {NoVersionsError} If the packument had no versions at all. */ export function tryResolvePackumentVersion( packument: UnityPackument, @@ -201,3 +200,16 @@ export function tryResolvePackumentVersion( return Ok(tryGetPackumentVersion(packument, requestedVersion)!); } + +/** + * Checks whether a packument has an entry for a version. + * @param packument The packument to check. + * @param version The version to check for. + */ +export function packumentHasVersion( + packument: UnityPackument, + version: SemanticVersion +): boolean { + const versions = recordKeys(packument.versions); + return versions.includes(version); +} diff --git a/src/domain/project-manifest.ts b/src/domain/project-manifest.ts index 9bd96986..aab48c18 100644 --- a/src/domain/project-manifest.ts +++ b/src/domain/project-manifest.ts @@ -148,23 +148,6 @@ export function addTestable( }; } -/** - * Prunes the manifest by performing the following operations: - * - Remove scoped-registries without scopes. - * @param manifest The manifest to prune. - */ -export function pruneManifest( - manifest: UnityProjectManifest -): UnityProjectManifest { - if (manifest.scopedRegistries === undefined) return manifest; - return { - ...manifest, - scopedRegistries: manifest.scopedRegistries.filter( - (it) => it.scopes.length > 0 - ), - }; -} - /** * Checks if a manifest has a dependency on a specific package. */ diff --git a/src/domain/registry-url.ts b/src/domain/registry-url.ts index e7b6bd26..2660e2ee 100644 --- a/src/domain/registry-url.ts +++ b/src/domain/registry-url.ts @@ -29,3 +29,5 @@ export function coerceRegistryUrl(s: string): RegistryUrl { } export const unityRegistryUrl = RegistryUrl.parse("https://packages.unity.com"); + +export const openupmRegistryUrl = RegistryUrl.parse("https://package.openupm.com"); diff --git a/src/domain/registry.ts b/src/domain/registry.ts index 9749d0d2..a5d8eb2c 100644 --- a/src/domain/registry.ts +++ b/src/domain/registry.ts @@ -1,4 +1,4 @@ -import { RegistryUrl } from "./registry-url"; +import { RegistryUrl, unityRegistryUrl } from "./registry-url"; import { NpmAuth } from "another-npm-registry-client"; /** @@ -15,3 +15,8 @@ export type Registry = Readonly<{ */ auth: NpmAuth | null; }>; + +export const unityRegistry: Registry = { + url: unityRegistryUrl, + auth: null, +}; diff --git a/src/domain/upm-config.ts b/src/domain/upm-config.ts index c1523475..5b1a73a6 100644 --- a/src/domain/upm-config.ts +++ b/src/domain/upm-config.ts @@ -1,39 +1,41 @@ -import { RegistryUrl } from "./registry-url"; -import { NpmAuth } from "another-npm-registry-client"; +import path from "path"; +import { CustomError } from "ts-custom-error"; -/** - * Abstraction of an upmconfig.toml file. - */ -export type UpmConfig = Readonly>; +const configFileName = ".upmconfig.toml"; -export const emptyUpmConfig: UpmConfig = {}; +export class NoSystemUserProfilePath extends CustomError {} /** - * Attempts to get the {@link NpmAuth} information for a specific registry - * from a {@link UpmConfig} object. - * @param upmConfig The config. - * @param registry The registry. - * @returns The auth information or null if the registry does not exist - * in the config. + * Determines the path to the users `.upmconfig.toml` file, based on the + * given parameters. + * @param envVars The current environment variables. + * @param homePath The users home path. + * @param forSystemUser Whether to get the path for the system user instead + * of the current one. + * @returns The resolved path. + * @throws {NoSystemUserProfilePath} When trying to get the path for the + * system-user but the `ALLUSERSPROFILE` env var is not set. + * @see https://docs.unity3d.com/Manual/upm-config.html */ -export function tryGetAuthForRegistry( - upmConfig: UpmConfig, - registry: RegistryUrl -): NpmAuth | null { - return upmConfig[registry] ?? null; -} +export function getUserUpmConfigPathFor( + envVars: Record, + homePath: string, + forSystemUser: boolean +): string { + function getConfigDirectory() { + const systemUserSubPath = "Unity/config/ServiceAccounts"; + if (forSystemUser) { + const profilePath = envVars["ALLUSERSPROFILE"]; + if (profilePath === undefined) throw new NoSystemUserProfilePath(); + return path.join(profilePath, systemUserSubPath); + } -/** - * Adds an entry to an upm-config. - * @param upmConfig The config. - * @param registry The registry for which to authenticate. - * @param auth The autentication information. - * @returns A new config with the entry added. - */ -export function addAuth( - upmConfig: UpmConfig, - registry: RegistryUrl, - auth: NpmAuth -): UpmConfig { - return { ...upmConfig, [registry]: auth }; + return homePath; + } + + const customDir = envVars["UPM_USER_CONFIG_FILE"]; + if (customDir !== undefined) return path.resolve(customDir); + + const directory = getConfigDirectory(); + return path.join(directory, configFileName); } diff --git a/src/io/all-packuments-io.ts b/src/io/all-packuments-io.ts index 723274e4..08036b79 100644 --- a/src/io/all-packuments-io.ts +++ b/src/io/all-packuments-io.ts @@ -1,10 +1,11 @@ import npmFetch from "npm-registry-fetch"; import { DomainName } from "../domain/domain-name"; import { Registry } from "../domain/registry"; -import { DebugLog, npmDebugLog } from "../logging"; -import { assertIsError, isHttpError } from "../utils/error-type-guards"; -import { RegistryAuthenticationError } from "./common-errors"; -import { getNpmFetchOptions, SearchedPackument } from "./npm-search"; +import { DebugLog } from "../logging"; +import { assertIsError } from "../utils/error-type-guards"; +import { makeRegistryInteractionError } from "./common-errors"; +import { makeNpmFetchOptions } from "./npm-registry"; +import { SearchedPackument } from "./npm-search"; /** * The result of querying the /-/all endpoint. @@ -19,34 +20,29 @@ export type AllPackuments = Readonly<{ * Function for getting all packuments from a npm registry. * @param registry The registry to get packuments for. */ -export type GetAllRegistryPackuments = (registry: Registry) => Promise; +export type GetAllRegistryPackuments = ( + registry: Registry +) => Promise; /** * Makes a {@link GetAllRegistryPackuments} function. */ -export function FetchAllRegistryPackuments(debugLog: DebugLog): GetAllRegistryPackuments { +export function getAllRegistryPackumentsUsing( + debugLog: DebugLog +): GetAllRegistryPackuments { return async (registry) => { debugLog(`Getting all packages from ${registry.url}.`); try { const result = await npmFetch.json( "/-/all", - getNpmFetchOptions(registry) + makeNpmFetchOptions(registry) ); return result as AllPackuments; } catch (error) { assertIsError(error); debugLog(`Failed to get all packages from ${registry.url}.`, error); - if (isHttpError(error)) - throw error.statusCode === 401 - ? new RegistryAuthenticationError(registry.url) - : error; - throw error; + throw makeRegistryInteractionError(error, registry.url); } }; } - -/** - * Default {@link GetAllRegistryPackuments} function. Uses {@link FetchAllRegistryPackuments}. - */ -export const getAllRegistryPackuments = FetchAllRegistryPackuments(npmDebugLog) \ No newline at end of file diff --git a/src/io/check-url.ts b/src/io/check-url.ts index 06b5f3c6..ce38a9cb 100644 --- a/src/io/check-url.ts +++ b/src/io/check-url.ts @@ -8,17 +8,10 @@ import fetch from "node-fetch"; export type CheckUrlExists = (url: string) => Promise; /** - * Makes a {@link CheckUrlExists} function that determines whether a url - * exists by checking whether it responds to a HEAD request with 200. + * {@link CheckUrlExists} function which uses {@link fetch} to send a + * `HEAD` request to the url and checks whether it is `ok`. */ -export function CheckUrlIsOk(): CheckUrlExists { - return async (url) => { - const response = await fetch(url, { method: "HEAD" }); - return response.status === 200; - }; -} - -/** - * Default {@link CheckUrlExists} function. Uses {@link CheckUrlIsOk}. - */ -export const checkUrlExists: CheckUrlExists = CheckUrlIsOk(); +export const fetchCheckUrlExists: CheckUrlExists = async (url) => { + const response = await fetch(url, { method: "HEAD" }); + return response.ok; +}; diff --git a/src/io/child-process.ts b/src/io/child-process.ts deleted file mode 100644 index 02ddc258..00000000 --- a/src/io/child-process.ts +++ /dev/null @@ -1,33 +0,0 @@ -import childProcess from "child_process"; -import { DebugLog, npmDebugLog } from "../logging"; - -/** - * Function that run a child process. - * @param command The command to run. - * @returns The commands standard output. - */ -export type RunChildProcess = (command: string) => Promise; - -/** - * Makes a {@link RunChildProcess} function which uses a promisified version of - * the built-in {@link childProcess.exec} function. - */ -function ExecChildProcess(debugLog: DebugLog): RunChildProcess { - return (command) => - new Promise(function (resolve, reject) { - childProcess.exec(command, function (error, stdout) { - if (error) { - debugLog("A child process failed.", error); - reject(error); - return; - } - - resolve(stdout); - }); - }); -} - -/** - * Default {@link RunChildProcess} function. Uses {@link ExecChildProcess}. - */ -export const runChildProcess = ExecChildProcess(npmDebugLog); diff --git a/src/io/common-errors.ts b/src/io/common-errors.ts index e1bb524f..811e9c7b 100644 --- a/src/io/common-errors.ts +++ b/src/io/common-errors.ts @@ -1,5 +1,6 @@ import { CustomError } from "ts-custom-error"; import { RegistryUrl } from "../domain/registry-url"; +import { isHttpError } from "../utils/error-type-guards"; /** * Error for when authentication with a registry failed. @@ -9,3 +10,34 @@ export class RegistryAuthenticationError extends CustomError { super(); } } + +/** + * Type for {@link Error}s with a HTTP status code property. + */ +export interface HttpErrorLike extends Error { + /** + * The HTTP status code. + */ + statusCode: number; +} + +/** + * Categorizes an error that occurred when interacting with a remote + * npm registry into the relevant domain errors. + * + * The logic goes like this. + * + * - Non-http {@link Error}s -> get returned as is. + * - Non-auth {@link HttpErrorLike}s -> get returned as is. + * - Auth {@link HttpErrorLike}s -> get converted to a {@link RegistryAuthenticationError}. + * @param error The error that occurred. + * @param registryUrl The url of the registry that was interacted with. + * @returns The categorized error. + */ +export function makeRegistryInteractionError( + error: Error, + registryUrl: RegistryUrl +): Error | HttpErrorLike | RegistryAuthenticationError { + if (!isHttpError(error) || error.statusCode !== 401) return error; + return new RegistryAuthenticationError(registryUrl); +} diff --git a/src/io/npm-registry.ts b/src/io/npm-registry.ts new file mode 100644 index 00000000..e7436749 --- /dev/null +++ b/src/io/npm-registry.ts @@ -0,0 +1,17 @@ +import { Registry } from "../domain/registry"; +import npmFetch from "npm-registry-fetch"; + +/** + * Converts a {@link Registry} object into a {@link npmFetch.Options} object for + * use in npm registry interactions. + * @param registry The registry object to convert. + * @returns The created options object. + */ +export function makeNpmFetchOptions(registry: Registry): npmFetch.Options { + const opts: npmFetch.Options = { + registry: registry.url, + }; + const auth = registry.auth; + if (auth !== null) Object.assign(opts, auth); + return opts; +} diff --git a/src/io/npm-search.ts b/src/io/npm-search.ts index 03b0a563..8d0ab1aa 100644 --- a/src/io/npm-search.ts +++ b/src/io/npm-search.ts @@ -1,11 +1,11 @@ import npmSearch from "libnpmsearch"; -import npmFetch from "npm-registry-fetch"; import { UnityPackument } from "../domain/packument"; import { Registry } from "../domain/registry"; import { SemanticVersion } from "../domain/semantic-version"; -import { DebugLog, npmDebugLog } from "../logging"; -import { assertIsHttpError } from "../utils/error-type-guards"; -import { RegistryAuthenticationError } from "./common-errors"; +import { DebugLog } from "../logging"; +import { assertIsError } from "../utils/error-type-guards"; +import { makeRegistryInteractionError } from "./common-errors"; +import { makeNpmFetchOptions } from "./npm-registry"; /** * A type representing a searched packument. Instead of having all versions @@ -28,39 +28,18 @@ export type SearchRegistry = ( keyword: string ) => Promise>; -/** - * Get npm fetch options. - */ -export const getNpmFetchOptions = function ( - registry: Registry -): npmFetch.Options { - const opts: npmSearch.Options = { - registry: registry.url, - }; - const auth = registry.auth; - if (auth !== null) Object.assign(opts, auth); - return opts; -}; - /** * Makes a {@link SearchRegistry} function which uses the npm search api to * find packages in a remote registry. */ -export function NpmApiSearch(debugLog: DebugLog): SearchRegistry { +export function searchRegistryUsing(debugLog: DebugLog): SearchRegistry { return (registry, keyword) => - npmSearch(keyword, getNpmFetchOptions(registry)) + npmSearch(keyword, makeNpmFetchOptions(registry)) // NOTE: The results of the search will be packument objects, so we can change the type .then((results) => results as SearchedPackument[]) .catch((error) => { - assertIsHttpError(error); + assertIsError(error); debugLog("A http request failed.", error); - throw error.statusCode === 401 - ? new RegistryAuthenticationError(registry.url) - : error; + throw makeRegistryInteractionError(error, registry.url); }); } - -/** - * Default {@link SearchRegistry} function. Uses {@link NpmApiSearch}. - */ -export const searchRegistry = NpmApiSearch(npmDebugLog); diff --git a/src/io/npmrc-io.ts b/src/io/npmrc-io.ts index 079bd221..de4012cb 100644 --- a/src/io/npmrc-io.ts +++ b/src/io/npmrc-io.ts @@ -1,7 +1,7 @@ import { EOL } from "node:os"; import path from "path"; import { Npmrc } from "../domain/npmrc"; -import { GetHomePath, getHomePathFromEnv } from "./special-paths"; +import { splitLines } from "../utils/string-utils"; import { readTextFile, ReadTextFile, @@ -19,9 +19,9 @@ export type FindNpmrcPath = () => string; * Makes a {@link FindNpmrcPath} function which resolves the path to the * `.npmrc` file that is stored in the users home directory. */ -export function FindNpmrcInHome(getHomePath: GetHomePath): FindNpmrcPath { +export function FindNpmrcInHome(homePath: string): FindNpmrcPath { + // TODO: Unsevice! This function is pure and does not benefit from being a service style function. return () => { - const homePath = getHomePath(); return path.join(homePath, ".npmrc"); }; } @@ -29,7 +29,8 @@ export function FindNpmrcInHome(getHomePath: GetHomePath): FindNpmrcPath { /** * Default {@link FindNpmrcPath} function. Uses {@link FindNpmrcInHome}. */ -export const findNpmrcPath: FindNpmrcPath = FindNpmrcInHome(getHomePathFromEnv); +export const findNpmrcPath = (homePath: string): FindNpmrcPath => + FindNpmrcInHome(homePath); /** * Function for loading npmrc. @@ -44,7 +45,12 @@ export type LoadNpmrc = (path: string) => Promise; */ export function ReadNpmrcFile(readFile: ReadTextFile): LoadNpmrc { return (path) => - readFile(path, true).then((content) => content?.split(EOL) ?? null); + readFile(path, true).then((content) => + content !== null + ? // TODO: Check if lines are valid. + splitLines(content) + : null + ); } /** diff --git a/src/io/packument-io.ts b/src/io/packument-io.ts index a83011a7..4d235902 100644 --- a/src/io/packument-io.ts +++ b/src/io/packument-io.ts @@ -1,11 +1,10 @@ import RegClient from "another-npm-registry-client"; -import { assertIsHttpError } from "../utils/error-type-guards"; -import { Registry } from "../domain/registry"; import { DomainName } from "../domain/domain-name"; import { UnityPackument } from "../domain/packument"; -import { RegistryAuthenticationError } from "./common-errors"; -import { DebugLog, npmDebugLog } from "../logging"; -import { npmRegistryClient } from "./reg-client"; +import { Registry } from "../domain/registry"; +import { DebugLog } from "../logging"; +import { assertIsHttpError } from "../utils/error-type-guards"; +import { makeRegistryInteractionError } from "./common-errors"; /** * Function for getting a packument from a registry. @@ -22,7 +21,7 @@ export type GetRegistryPackument = ( * Makes a {@link GetRegistryPackument} function which fetches the packument * from a remote npm registry. */ -export function FetchRegistryPackument( +export function getRegistryPackumentUsing( registryClient: RegClient.Instance, debugLog: DebugLog ): GetRegistryPackument { @@ -37,23 +36,10 @@ export function FetchRegistryPackument( debugLog("Fetching a packument failed.", error); assertIsHttpError(error); if (error.statusCode === 404) resolve(null); - else - reject( - error.statusCode === 401 - ? new RegistryAuthenticationError(registry.url) - : error - ); + else reject(makeRegistryInteractionError(error, registry.url)); } else resolve(packument); } ); }); }; } - -/** - * Default {@link GetRegistryPackument} function. Uses {@link FetchRegistryPackument}. - */ -export const getRegistryPackument = FetchRegistryPackument( - npmRegistryClient, - npmDebugLog -); diff --git a/src/io/project-manifest-io.ts b/src/io/project-manifest-io.ts index 8c8099da..c024f9a9 100644 --- a/src/io/project-manifest-io.ts +++ b/src/io/project-manifest-io.ts @@ -1,11 +1,8 @@ -import { AnyJson } from "@iarna/toml"; import path from "path"; import { CustomError } from "ts-custom-error"; -import { - pruneManifest, - UnityProjectManifest, -} from "../domain/project-manifest"; -import { DebugLog, npmDebugLog } from "../logging"; +import { z } from "zod"; +import { UnityProjectManifest } from "../domain/project-manifest"; +import { DebugLog } from "../logging"; import { assertIsError } from "../utils/error-type-guards"; import { readTextFile, @@ -40,6 +37,20 @@ export type LoadProjectManifest = ( projectPath: string ) => Promise; +// TODO: Add a better schema +const projectManifestSchema = z.object({}).passthrough(); + +/** + * Parses the content of a `manifest.json` file to a {@link UnityProjectManifest}. + * @param content The files content. + * @returns The parsed file. + * @throws {Error} If parsing failed. + */ +export function parseProjectManifest(content: string): UnityProjectManifest { + const json = JSON.parse(content); + return projectManifestSchema.parse(json) as UnityProjectManifest; +} + /** * Makes a {@link LoadProjectManifest} function which reads the content * of a `manifest.json` file. @@ -54,29 +65,21 @@ export function ReadProjectManifestFile( const content = await readFile(manifestPath, true); if (content === null) throw new ManifestMissingError(manifestPath); - let json: AnyJson; try { - json = await JSON.parse(content); + return parseProjectManifest(content); } catch (error) { assertIsError(error); debugLog("Manifest parse failed because of invalid json content.", error); throw new ManifestMalformedError(); } - - // TODO: Actually validate the json structure - if (typeof json !== "object") throw new ManifestMalformedError(); - - return json as unknown as UnityProjectManifest; }; } /** * Default {@link LoadProjectManifest} function. Uses {@link ReadProjectManifestFile}. */ -export const loadProjectManifest: LoadProjectManifest = ReadProjectManifestFile( - readTextFile, - npmDebugLog -); +export const loadProjectManifestUsing = (debugLog: DebugLog) => + ReadProjectManifestFile(readTextFile, debugLog); /** * Function for replacing the project manifest for a Unity project. @@ -88,6 +91,26 @@ export type SaveProjectManifest = ( manifest: UnityProjectManifest ) => Promise; +/** + * Serializes a {@link UnityProjectManifest} object into json format. + * @param manifest The manifest to serialize. + * @returns The serialized manifest. + */ +export function serializeProjectManifest( + manifest: UnityProjectManifest +): string { + // Remove empty scoped registries + if (manifest.scopedRegistries !== undefined) + manifest = { + ...manifest, + scopedRegistries: manifest.scopedRegistries.filter( + (it) => it.scopes.length > 0 + ), + }; + + return JSON.stringify(manifest, null, 2); +} + /** * Makes a {@link SaveProjectManifest} function which overwrites the contents * of a `manifest.json` file. @@ -97,10 +120,8 @@ export function WriteProjectManifestFile( ): SaveProjectManifest { return async (projectPath, manifest) => { const manifestPath = manifestPathFor(projectPath); - manifest = pruneManifest(manifest); - const json = JSON.stringify(manifest, null, 2); - - return await writeFile(manifestPath, json); + const content = serializeProjectManifest(manifest); + return await writeFile(manifestPath, content); }; } diff --git a/src/io/project-version-io.ts b/src/io/project-version-io.ts index d50b13bc..0e08940b 100644 --- a/src/io/project-version-io.ts +++ b/src/io/project-version-io.ts @@ -2,8 +2,10 @@ import { AnyJson } from "@iarna/toml"; import path from "path"; import { CustomError } from "ts-custom-error"; import * as YAML from "yaml"; -import { DebugLog, npmDebugLog } from "../logging"; +import { z } from "zod"; +import { DebugLog } from "../logging"; import { assertIsError } from "../utils/error-type-guards"; +import { isZod } from "../utils/zod-utils"; import { readTextFile, ReadTextFile } from "./text-file-io"; export class ProjectVersionMissingError extends CustomError { @@ -31,6 +33,8 @@ export function projectVersionTxtPathFor(projectDirPath: string) { */ export type GetProjectVersion = (projectDirPath: string) => Promise; +const projectVersionTxtSchema = z.object({ m_EditorVersion: z.string() }); + /** * Makes a {@link GetProjectVersion} function which gets the project-version * from the `ProjectSettings/ProjectVersion.txt` file. @@ -54,14 +58,7 @@ export function ReadProjectVersionFile( throw new ProjectVersionMalformedError(); } - if ( - !( - typeof yaml === "object" && - yaml !== null && - "m_EditorVersion" in yaml && - typeof yaml.m_EditorVersion === "string" - ) - ) + if (!isZod(yaml, projectVersionTxtSchema)) throw new ProjectVersionMalformedError(); return yaml.m_EditorVersion; @@ -71,7 +68,5 @@ export function ReadProjectVersionFile( /** * Default {@link GetProjectVersion} function. Uses {@link ReadProjectVersionFile}. */ -export const getProjectVersion = ReadProjectVersionFile( - readTextFile, - npmDebugLog -); +export const getProjectVersionUsing = (debugLog: DebugLog) => + ReadProjectVersionFile(readTextFile, debugLog); diff --git a/src/io/reg-client.ts b/src/io/reg-client.ts index 92e26d01..5d143626 100644 --- a/src/io/reg-client.ts +++ b/src/io/reg-client.ts @@ -1,6 +1,8 @@ import RegClient from "another-npm-registry-client"; +import { Logger } from "npmlog"; /** * Client for communicating with the npm registry. */ -export const npmRegistryClient = new RegClient(); +export const makeNpmRegistryClient = (debugLogger: Logger) => + new RegClient({ log: debugLogger }); diff --git a/src/io/special-paths.ts b/src/io/special-paths.ts index 4e4a5c23..9d2598f6 100644 --- a/src/io/special-paths.ts +++ b/src/io/special-paths.ts @@ -1,6 +1,7 @@ -import { Err, Ok, Result } from "ts-results-es"; import os from "os"; import { CustomError } from "ts-custom-error"; +import { Err, Ok, Result } from "ts-results-es"; +import { EditorVersionNotSupportedError } from "../common-errors"; import { compareEditorVersion, EditorVersion, @@ -8,8 +9,6 @@ import { ReleaseVersion, stringifyEditorVersion, } from "../domain/editor-version"; -import { EditorVersionNotSupportedError } from "../common-errors"; -import { tryGetEnv } from "../utils/env-util"; /** * Error for when a specific OS does not support a specific editor-version. @@ -43,24 +42,22 @@ export class OSNotSupportedError extends CustomError { } } -/** - * Function for getting the path of the users home directory. - * @returns The path to the directory. - */ -export type GetHomePath = () => string; - export class NoHomePathError extends CustomError {} /** - * {@link GetHomePath} function which resolved the home path using the - * `USERPROFILE` and `HOME` environment variables. - * @throws NoHomePathError if none of the required env variables are set. + * Attempts to resolve the users home path from environment variables, + * specifically the `USERPROFILE` and `HOME` vars. + * @param envVars The current environment variables. + * @returns The resolved path. + * @throws {NoHomePathError} If none of the required env vars were set. */ -export const getHomePathFromEnv: GetHomePath = () => { - const homePath = tryGetEnv("USERPROFILE") ?? tryGetEnv("HOME"); - if (homePath === null) throw new NoHomePathError(); +export function getHomePathFromEnv( + envVars: Record +) { + const homePath = envVars["USERPROFILE"] ?? envVars["HOME"]; + if (homePath === undefined) throw new NoHomePathError(); return homePath; -}; +} /** * Errors which may occur when trying to resolve an editor-version. @@ -108,14 +105,3 @@ export function tryGetEditorInstallPath( return Err(new OSNotSupportedError(platform)); } - -/** - * Function that gets the current working directories path. - * @returns The path. - */ -export type GetCwd = () => string; - -/** - * {@link GetCwd} function which uses {@link process.cwd}. - */ -export const getProcessCwd: GetCwd = process.cwd; diff --git a/src/io/upm-config-io.ts b/src/io/upm-config-io.ts index 1c06ddd2..54eec4bd 100644 --- a/src/io/upm-config-io.ts +++ b/src/io/upm-config-io.ts @@ -1,14 +1,11 @@ import TOML from "@iarna/toml"; -import path from "path"; -import { CustomError } from "ts-custom-error"; import { z } from "zod"; import { Base64 } from "../domain/base64"; -import { tryGetEnv } from "../utils/env-util"; +import { getUserUpmConfigPathFor } from "../domain/upm-config"; import { removeExplicitUndefined, RemoveExplicitUndefined, } from "../utils/zod-utils"; -import { GetHomePath, getHomePathFromEnv } from "./special-paths"; import { readTextFile, ReadTextFile, @@ -16,16 +13,12 @@ import { WriteTextFile, } from "./text-file-io"; -const configFileName = ".upmconfig.toml"; - -export class NoSystemUserProfilePath extends CustomError {} - /** * Function which gets the path to the upmconfig file. * @param systemUser Whether to authenticate as a Windows system-user. * @returns The file path. */ -export type GetUpmConfigPath = (systemUser: boolean) => Promise; +export type GetUpmConfigPath = (systemUser: boolean) => string; /** * Makes a {@link GetUpmConfigPath} function which resolves to the default @@ -33,33 +26,17 @@ export type GetUpmConfigPath = (systemUser: boolean) => Promise; * @see https://docs.unity3d.com/Manual/upm-config.html#upmconfig */ export function ResolveDefaultUpmConfigPath( - getHomePath: GetHomePath + homePath: string ): GetUpmConfigPath { - async function getConfigDirectory(systemUser: boolean) { - const systemUserSubPath = "Unity/config/ServiceAccounts"; - if (systemUser) { - const profilePath = tryGetEnv("ALLUSERSPROFILE"); - if (profilePath === null) throw new NoSystemUserProfilePath(); - return path.join(profilePath, systemUserSubPath); - } - - return getHomePath(); - } - - return async (systemUser) => { - const customDir = tryGetEnv("UPM_USER_CONFIG_FILE"); - if (customDir !== null) return path.resolve(customDir); - - const directory = await getConfigDirectory(systemUser); - return path.join(directory, configFileName); - }; + return (systemUser) => + getUserUpmConfigPathFor(process.env, homePath, systemUser); } /** * Default {@link GetUpmConfigPath} function. Uses {@link ResolveDefaultUpmConfigPath}. */ -export const getUpmConfigPath: GetUpmConfigPath = - ResolveDefaultUpmConfigPath(getHomePathFromEnv); +export const getUpmConfigPath = (homePath: string): GetUpmConfigPath => + ResolveDefaultUpmConfigPath(homePath); const authBaseSchema = z.object({ alwaysAuth: z.optional(z.boolean()), diff --git a/src/logging.ts b/src/logging.ts index 7956f107..d15cfc06 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,5 +1,3 @@ -import npmlog from "npmlog"; - /** * Function that logs a message to some external system. This interface is * output-agnostic meaning that the logs could go to a console, file or other. @@ -16,19 +14,3 @@ export type DebugLog = ( ) => void | Promise; export const noopLogger: DebugLog = () => {}; - -/** - * {@link DebugLog} function which uses {@link npmlog} to print logs to - * the console. - */ -export const npmDebugLog: DebugLog = function (message, context) { - const contextMessage = - context !== undefined - ? `\n${ - context instanceof Error - ? context.toString() - : JSON.stringify(context, null, 2) - }` - : ""; - return npmlog.verbose("", `${message}${contextMessage}`); -}; diff --git a/src/services/built-in-package-check.ts b/src/services/built-in-package-check.ts index 237ed656..b41d9f82 100644 --- a/src/services/built-in-package-check.ts +++ b/src/services/built-in-package-check.ts @@ -1,12 +1,14 @@ +import RegClient from "another-npm-registry-client"; import { DomainName } from "../domain/domain-name"; +import { packumentHasVersion } from "../domain/packument"; import { unityRegistryUrl } from "../domain/registry-url"; import { SemanticVersion } from "../domain/semantic-version"; -import { getRegistryPackument, GetRegistryPackument } from "../io/packument-io"; -import { recordKeys } from "../utils/record-utils"; +import { CheckUrlExists, fetchCheckUrlExists } from "../io/check-url"; import { - checkIsUnityPackage, - CheckIsUnityPackage, -} from "./unity-package-check"; + GetRegistryPackument, + getRegistryPackumentUsing, +} from "../io/packument-io"; +import { DebugLog } from "../logging"; /** * Function for checking whether a specific package version is built-in. @@ -19,15 +21,25 @@ export type CheckIsBuiltInPackage = ( version: SemanticVersion ) => Promise; +function docUrlForPackage(packageName: DomainName) { + return `https://docs.unity3d.com/Manual/${packageName}.html`; +} + /** * Makes a {@link CheckIsBuiltInPackage} function which checks if package is * built-in by establishing if the package is an official Unity package which * does not exist on the Unity package registry. */ export function CheckIsNonRegistryUnityPackage( - checkIsUnityPackage: CheckIsUnityPackage, + checkUrlExists: CheckUrlExists, getRegistryPackument: GetRegistryPackument ): CheckIsBuiltInPackage { + function checkIsUnityPackage(packageName: DomainName) { + // A package is an official Unity package if it has a documentation page + const url = docUrlForPackage(packageName); + return checkUrlExists(url); + } + async function checkExistsOnUnityRegistry( packageName: DomainName, version: SemanticVersion @@ -37,8 +49,7 @@ export function CheckIsNonRegistryUnityPackage( packageName ); if (packument === null) return false; - const versions = recordKeys(packument.versions); - return versions.includes(version); + return packumentHasVersion(packument, version); } return async (packageName, version) => { @@ -51,7 +62,11 @@ export function CheckIsNonRegistryUnityPackage( /** * Default {@link CheckIsBuiltInPackage}. Uses {@link CheckIsNonRegistryUnityPackage}. */ -export const checkIsBuiltInPackage = CheckIsNonRegistryUnityPackage( - checkIsUnityPackage, - getRegistryPackument -); +export const checkIsBuiltInPackage = ( + registryClient: RegClient.Instance, + debugLog: DebugLog +) => + CheckIsNonRegistryUnityPackage( + fetchCheckUrlExists, + getRegistryPackumentUsing(registryClient, debugLog) + ); diff --git a/src/services/dependency-resolving.ts b/src/services/dependency-resolving.ts index 58f66595..be8e20f9 100644 --- a/src/services/dependency-resolving.ts +++ b/src/services/dependency-resolving.ts @@ -1,3 +1,4 @@ +import RegClient from "another-npm-registry-client"; import { PackumentNotFoundError } from "../common-errors"; import { DependencyGraph, @@ -15,7 +16,11 @@ import { import { Registry } from "../domain/registry"; import { RegistryUrl } from "../domain/registry-url"; import { SemanticVersion } from "../domain/semantic-version"; -import { getRegistryPackument, GetRegistryPackument } from "../io/packument-io"; +import { + GetRegistryPackument, + getRegistryPackumentUsing, +} from "../io/packument-io"; +import { DebugLog } from "../logging"; import { checkIsBuiltInPackage, CheckIsBuiltInPackage, @@ -100,7 +105,11 @@ export function ResolveDependenciesFromRegistries( /** * Default {@link ResolveDependencies} function. Uses {@link ResolveDependenciesFromRegistries}. */ -export const resolveDependencies = ResolveDependenciesFromRegistries( - getRegistryPackument, - checkIsBuiltInPackage -); +export const resolveDependencies = ( + registryClient: RegClient.Instance, + debugLog: DebugLog +) => + ResolveDependenciesFromRegistries( + getRegistryPackumentUsing(registryClient, debugLog), + checkIsBuiltInPackage(registryClient, debugLog) + ); diff --git a/src/services/determine-editor-version.ts b/src/services/determine-editor-version.ts index 907e3139..140a3c97 100644 --- a/src/services/determine-editor-version.ts +++ b/src/services/determine-editor-version.ts @@ -3,7 +3,11 @@ import { ReleaseVersion, tryParseEditorVersion, } from "../domain/editor-version"; -import { getProjectVersion, GetProjectVersion } from "../io/project-version-io"; +import { + GetProjectVersion, + getProjectVersionUsing, +} from "../io/project-version-io"; +import { DebugLog } from "../logging"; /** * Function for determining the editor-version for a Unity project. @@ -34,5 +38,5 @@ export function DetermineEditorVersionFromFile( /** * Default {@link DetermineEditorVersion} function. Uses {@link DetermineEditorVersionFromFile}. */ -export const determineEditorVersion = - DetermineEditorVersionFromFile(getProjectVersion); +export const determineEditorVersionUsing = (debugLog: DebugLog) => + DetermineEditorVersionFromFile(getProjectVersionUsing(debugLog)); diff --git a/src/services/get-auth-token.ts b/src/services/get-auth-token.ts index ca0bbf11..05d315b9 100644 --- a/src/services/get-auth-token.ts +++ b/src/services/get-auth-token.ts @@ -1,8 +1,7 @@ import RegClient from "another-npm-registry-client"; import { RegistryUrl } from "../domain/registry-url"; import { RegistryAuthenticationError } from "../io/common-errors"; -import { npmRegistryClient } from "../io/reg-client"; -import { DebugLog, npmDebugLog } from "../logging"; +import { DebugLog } from "../logging"; /** * A token authenticating a user. @@ -54,7 +53,7 @@ export function AuthenticateWithNpmRegistry( /** * Default {@link GetAuthToken}. Uses {@link AuthenticateWithNpmRegistry}. */ -export const getAuthToken = AuthenticateWithNpmRegistry( - npmRegistryClient, - npmDebugLog -); +export const getAuthToken = ( + registryClient: RegClient.Instance, + debugLog: DebugLog +) => AuthenticateWithNpmRegistry(registryClient, debugLog); diff --git a/src/services/get-latest-version.ts b/src/services/get-latest-version.ts index 038e8a31..1b8c231e 100644 --- a/src/services/get-latest-version.ts +++ b/src/services/get-latest-version.ts @@ -1,8 +1,13 @@ +import RegClient from "another-npm-registry-client"; import { DomainName } from "../domain/domain-name"; import { tryResolvePackumentVersion } from "../domain/packument"; import { Registry } from "../domain/registry"; import { SemanticVersion } from "../domain/semantic-version"; -import { getRegistryPackument, GetRegistryPackument } from "../io/packument-io"; +import { + GetRegistryPackument, + getRegistryPackumentUsing, +} from "../io/packument-io"; +import { DebugLog } from "../logging"; /** * Gets the latest published version of a package from a npm registry. @@ -34,5 +39,10 @@ export function GetLatestVersionFromRegistryPackument( /** * Default {@link GetLatestVersion}. Uses {@link GetLatestVersionFromRegistryPackument}. */ -export const getLatestVersion = - GetLatestVersionFromRegistryPackument(getRegistryPackument); +export const getLatestVersion = ( + registryClient: RegClient.Instance, + debugLog: DebugLog +) => + GetLatestVersionFromRegistryPackument( + getRegistryPackumentUsing(registryClient, debugLog) + ); diff --git a/src/services/get-registry-auth.ts b/src/services/get-registry-auth.ts index c86cdb7c..252c9af6 100644 --- a/src/services/get-registry-auth.ts +++ b/src/services/get-registry-auth.ts @@ -1,74 +1,102 @@ import { NpmAuth } from "another-npm-registry-client"; import { decodeBase64 } from "../domain/base64"; -import { coerceRegistryUrl } from "../domain/registry-url"; -import { addAuth, emptyUpmConfig, UpmConfig } from "../domain/upm-config"; +import { Registry } from "../domain/registry"; import { + openupmRegistryUrl, + RegistryUrl, + unityRegistryUrl, +} from "../domain/registry-url"; +import { + getUpmConfigPath, + GetUpmConfigPath, loadUpmConfig, LoadUpmConfig, UpmAuth, UpmConfigContent, } from "../io/upm-config-io"; -import { recordEntries } from "../utils/record-utils"; +import { DebugLog } from "../logging"; import { trySplitAtFirstOccurrenceOf } from "../utils/string-utils"; import { removeExplicitUndefined } from "../utils/zod-utils"; /** * Service function for getting registry authentication. - * @param upmConfigPath The path to the upmconfig.toml file. + * @param systemUser Whether to authenticate as a Windows system-user. + * @param url The url for which to get authentication. */ -export type GetRegistryAuth = (upmConfigPath: string) => Promise; +export type GetRegistryAuth = ( + systemUser: boolean, + url: RegistryUrl +) => Promise; /** * Makes a {@link GetRegistryAuth} function which gets it's information from * the users upm config. */ export function LoadRegistryAuthFromUpmConfig( - loadUpmConfig: LoadUpmConfig + getUpmConfigPath: GetUpmConfigPath, + loadUpmConfig: LoadUpmConfig, + debugLog: DebugLog ): GetRegistryAuth { - return async (upmConfigPath) => { - function importNpmAuth(input: UpmAuth): NpmAuth { - // Basic auth - if ("_auth" in input) { - const decoded = decodeBase64(input._auth); - const [username, password] = trySplitAtFirstOccurrenceOf(decoded, ":"); - if (password === null) - throw new Error( - "Auth Base64 string was not in the user:pass format." - ); - return removeExplicitUndefined({ - username, - password, - email: input.email, - alwaysAuth: input.alwaysAuth, - }); - } - - // Bearer auth + function importNpmAuth(input: UpmAuth): NpmAuth { + // Basic auth + if ("_auth" in input) { + const decoded = decodeBase64(input._auth); + const [username, password] = trySplitAtFirstOccurrenceOf(decoded, ":"); + if (password === null) + throw new Error("Auth Base64 string was not in the user:pass format."); return removeExplicitUndefined({ - token: input.token, + username, + password, + email: input.email, alwaysAuth: input.alwaysAuth, }); } - function importUpmConfig(input: UpmConfigContent): UpmConfig { - if (input.npmAuth === undefined) return {}; - return recordEntries(input.npmAuth) - .map( - ([url, auth]) => - [coerceRegistryUrl(url), importNpmAuth(auth)] as const - ) - .reduce( - (upmConfig, [url, auth]) => addAuth(upmConfig, url, auth), - emptyUpmConfig + return removeExplicitUndefined({ + token: input.token, + alwaysAuth: input.alwaysAuth, + }); + } + + let cachedConfig: UpmConfigContent | null = null; + + return async (systemUser, url) => { + // We know there is no auth required for these registries + if (url === openupmRegistryUrl || url === unityRegistryUrl) + return { url, auth: null }; + + // Only load config if we have dont have it in the cache + if (cachedConfig === null) { + const configPath = getUpmConfigPath(systemUser); + cachedConfig = await loadUpmConfig(configPath); + if (cachedConfig === null) { + debugLog( + `No .upmconfig.toml file found. Will use no auth for registry "${url}".` ); + return { url, auth: null }; + } + } + + const entry = + cachedConfig.npmAuth?.[url] ?? cachedConfig?.npmAuth?.[url + "/"] ?? null; + if (entry === null) { + debugLog( + `.upmconfig.toml had no entry for registry "${url}". Will not use auth for that registry.` + ); + return { url, auth: null }; } - const content = await loadUpmConfig(upmConfigPath); - return content !== null ? importUpmConfig(content) : emptyUpmConfig; + const auth = importNpmAuth(entry); + return { url, auth }; }; } /** * Default {@link GetRegistryAuth} function. Uses {@link LoadRegistryAuthFromUpmConfig}. */ -export const getRegistryAuth = LoadRegistryAuthFromUpmConfig(loadUpmConfig); +export const getRegistryAuthUsing = (homePath: string, debugLog: DebugLog) => + LoadRegistryAuthFromUpmConfig( + getUpmConfigPath(homePath), + loadUpmConfig, + debugLog + ); diff --git a/src/services/get-registry-packument-version.ts b/src/services/get-registry-packument-version.ts index df8ec50a..2fe68bfe 100644 --- a/src/services/get-registry-packument-version.ts +++ b/src/services/get-registry-packument-version.ts @@ -1,3 +1,4 @@ +import RegClient from "another-npm-registry-client"; import { AsyncResult, Err } from "ts-results-es"; import { PackumentNotFoundError } from "../common-errors"; import { DomainName } from "../domain/domain-name"; @@ -10,7 +11,11 @@ import { } from "../domain/packument"; import { Registry } from "../domain/registry"; import { RegistryUrl } from "../domain/registry-url"; -import { getRegistryPackument, GetRegistryPackument } from "../io/packument-io"; +import { + GetRegistryPackument, + getRegistryPackumentUsing, +} from "../io/packument-io"; +import { DebugLog } from "../logging"; import { resultifyAsyncOp } from "../utils/result-utils"; /** @@ -69,5 +74,8 @@ export function FetchRegistryPackumentVersion( /** * Default {@link GetRegistryPackumentVersion} function. Uses {@link FetchRegistryPackumentVersion}. */ -export const getRegistryPackumentVersion = - FetchRegistryPackumentVersion(getRegistryPackument); +export const getRegistryPackumentVersionUsing = ( + regClient: RegClient.Instance, + debugLog: DebugLog +) => + FetchRegistryPackumentVersion(getRegistryPackumentUsing(regClient, debugLog)); diff --git a/src/services/login.ts b/src/services/login.ts index e54a28bd..5757aedf 100644 --- a/src/services/login.ts +++ b/src/services/login.ts @@ -1,6 +1,6 @@ -import { NpmAuth } from "another-npm-registry-client"; +import RegClient, { NpmAuth } from "another-npm-registry-client"; import { RegistryUrl } from "../domain/registry-url"; -import { DebugLog, npmDebugLog } from "../logging"; +import { DebugLog } from "../logging"; import { getAuthToken, GetAuthToken } from "./get-auth-token"; import { putNpmAuthToken, StoreNpmAuthToken } from "./put-npm-auth-token"; import { putRegistryAuth, PutRegistryAuth } from "./put-registry-auth"; @@ -66,9 +66,14 @@ export function UpmConfigLogin( /** * Default {@link Login} function. Uses {@link UpmConfigLogin}. */ -export const login = UpmConfigLogin( - putRegistryAuth, - getAuthToken, - putNpmAuthToken, - npmDebugLog -); +export const login = ( + homePath: string, + registryClient: RegClient.Instance, + debugLog: DebugLog +) => + UpmConfigLogin( + putRegistryAuth, + getAuthToken(registryClient, debugLog), + putNpmAuthToken(homePath), + debugLog + ); diff --git a/src/services/parse-env.ts b/src/services/parse-env.ts index 6a7974db..27ee4e0c 100644 --- a/src/services/parse-env.ts +++ b/src/services/parse-env.ts @@ -3,15 +3,12 @@ import { Logger } from "npmlog"; import path from "path"; import { CustomError } from "ts-custom-error"; import { CmdOptions } from "../cli/options"; -import { Registry } from "../domain/registry"; -import { coerceRegistryUrl, RegistryUrl } from "../domain/registry-url"; -import { tryGetAuthForRegistry, UpmConfig } from "../domain/upm-config"; -import { GetCwd } from "../io/special-paths"; -import { GetUpmConfigPath } from "../io/upm-config-io"; -import { DebugLog } from "../logging"; +import { + coerceRegistryUrl, + openupmRegistryUrl, + RegistryUrl, +} from "../domain/registry-url"; import { tryGetEnv } from "../utils/env-util"; -import { assertIsError } from "../utils/error-type-guards"; -import { GetRegistryAuth } from "./get-registry-auth"; /** * Error for when auth information for a registry could not be loaded. @@ -33,17 +30,13 @@ export type Env = Readonly<{ */ systemUser: boolean; /** - * Whether to fall back to the upstream registry. + * Whether to fall back to the Unity registry. */ upstream: boolean; /** - * The upstream registry. + * The primary registry url. */ - upstreamRegistry: Registry; - /** - * The primary registry. - */ - registry: Registry; + primaryRegistryUrl: RegistryUrl; }>; /** @@ -57,46 +50,11 @@ export type ParseEnv = (options: CmdOptions) => Promise; /** * Creates a {@link ParseEnv} function. */ -export function makeParseEnv( - log: Logger, - getUpmConfigPath: GetUpmConfigPath, - getRegistryAuth: GetRegistryAuth, - getCwd: GetCwd, - debugLog: DebugLog -): ParseEnv { +export function makeParseEnv(log: Logger, processCwd: string): ParseEnv { function determineCwd(options: CmdOptions): string { - return options.chdir !== undefined ? path.resolve(options.chdir) : getCwd(); - } - - function determinePrimaryRegistry( - options: CmdOptions, - upmConfig: UpmConfig - ): Registry { - if (options.registry === undefined) { - return { - url: RegistryUrl.parse("https://package.openupm.com"), - auth: null, - }; - } - - const url = coerceRegistryUrl(options.registry); - - const auth = tryGetAuthForRegistry(upmConfig, url); - - if (auth === null) { - log.verbose( - "", - `upm config did not contain an entry for "${url}". Will use this registry without authentication.` - ); - } - - return { url, auth }; - } - - function determineUpstreamRegistry(): Registry { - const url = RegistryUrl.parse("https://packages.unity.com"); - - return { url, auth: null }; + return options.chdir !== undefined + ? path.resolve(options.chdir) + : processCwd; } function determineLogLevel(options: CmdOptions): "verbose" | "notice" { @@ -115,6 +73,11 @@ export function makeParseEnv( return options.systemUser === true; } + function determinePrimaryRegistryUrl(options: CmdOptions): RegistryUrl { + if (options.registry === undefined) return openupmRegistryUrl; + return coerceRegistryUrl(options.registry); + } + return async (options) => { // log level log.level = determineLogLevel(options); @@ -133,29 +96,16 @@ export function makeParseEnv( const systemUser = determineIsSystemUser(options); // registries - const upmConfigPath = await getUpmConfigPath(systemUser); - - let registry: Registry; - let upstreamRegistry: Registry; - try { - const upmConfig = await getRegistryAuth(upmConfigPath); - registry = determinePrimaryRegistry(options, upmConfig); - upstreamRegistry = determineUpstreamRegistry(); - } catch (error) { - assertIsError(error); - debugLog("Upmconfig load or parsing failed.", error); - throw new RegistryAuthLoadError(); - } + const primaryRegistryUrl = determinePrimaryRegistryUrl(options); // cwd const cwd = determineCwd(options); return { cwd, - registry, + primaryRegistryUrl, systemUser, upstream, - upstreamRegistry, }; }; } diff --git a/src/services/put-npm-auth-token.ts b/src/services/put-npm-auth-token.ts index dc65472f..77ae4d49 100644 --- a/src/services/put-npm-auth-token.ts +++ b/src/services/put-npm-auth-token.ts @@ -41,8 +41,5 @@ export function StoreNpmAuthTokenInNpmrc( /** * Default {@link StoreNpmAuthToken} function. Uses {@link StoreNpmAuthTokenInNpmrc}. */ -export const putNpmAuthToken = StoreNpmAuthTokenInNpmrc( - findNpmrcPath, - loadNpmrc, - saveNpmrc -); +export const putNpmAuthToken = (homePath: string) => + StoreNpmAuthTokenInNpmrc(findNpmrcPath(homePath), loadNpmrc, saveNpmrc); diff --git a/src/services/remove-packages.ts b/src/services/remove-packages.ts index 054d55ee..39b2ecd2 100644 --- a/src/services/remove-packages.ts +++ b/src/services/remove-packages.ts @@ -8,11 +8,12 @@ import { } from "../domain/project-manifest"; import { SemanticVersion } from "../domain/semantic-version"; import { - loadProjectManifest, LoadProjectManifest, + loadProjectManifestUsing, saveProjectManifest, SaveProjectManifest, } from "../io/project-manifest-io"; +import { DebugLog } from "../logging"; import { resultifyAsyncOp } from "../utils/result-utils"; /** @@ -61,7 +62,7 @@ export function RemovePackagesFromManifest( manifest = removeDependency(manifest, packageName); - if (manifest.scopedRegistries !== undefined) + if (manifest.scopedRegistries !== undefined) { manifest = { ...manifest, scopedRegistries: manifest.scopedRegistries @@ -74,6 +75,36 @@ export function RemovePackagesFromManifest( })), }; + // Remove scoped registries without scopes + manifest = { + ...manifest, + scopedRegistries: manifest.scopedRegistries!.filter( + (it) => it.scopes.length > 0 + ), + }; + + // Remove scoped registries property if empty + if (manifest.scopedRegistries!.length === 0) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { scopedRegistries, ...withoutScopedRegistries } = manifest; + manifest = withoutScopedRegistries; + } + } + + if (manifest.testables !== undefined) { + manifest = { + ...manifest, + testables: manifest.testables.filter((it) => it !== packageName), + }; + + // Remove testables property if empty + if (manifest.testables!.length === 0) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { testables, ...withoutTestables } = manifest; + manifest = withoutTestables; + } + } + return Ok([manifest, { name: packageName, version: versionInManifest }]); }; @@ -122,7 +153,8 @@ export function RemovePackagesFromManifest( /** * Default {@link RemovePackages} function. Uses {@link RemovePackagesFromManifest}. */ -export const removePackages = RemovePackagesFromManifest( - loadProjectManifest, - saveProjectManifest -); +export const removePackages = (debugLog: DebugLog) => + RemovePackagesFromManifest( + loadProjectManifestUsing(debugLog), + saveProjectManifest + ); diff --git a/src/services/search-packages.ts b/src/services/search-packages.ts index 44ba87d2..f2e73ae4 100644 --- a/src/services/search-packages.ts +++ b/src/services/search-packages.ts @@ -1,14 +1,14 @@ import { Registry } from "../domain/registry"; import { - getAllRegistryPackuments, GetAllRegistryPackuments, + getAllRegistryPackumentsUsing, } from "../io/all-packuments-io"; import { SearchedPackument, - searchRegistry, SearchRegistry, + searchRegistryUsing, } from "../io/npm-search"; -import { DebugLog, npmDebugLog } from "../logging"; +import { DebugLog } from "../logging"; import { assertIsError } from "../utils/error-type-guards"; /** @@ -65,8 +65,9 @@ export function ApiAndFallbackPackageSearch( /** * Default {@link SearchPackages} function. Uses {@link ApiAndFallbackPackageSearch}. */ -export const searchPackages = ApiAndFallbackPackageSearch( - searchRegistry, - getAllRegistryPackuments, - npmDebugLog -); +export const searchPackagesUsing = (debugLog: DebugLog) => + ApiAndFallbackPackageSearch( + searchRegistryUsing(debugLog), + getAllRegistryPackumentsUsing(debugLog), + debugLog + ); diff --git a/src/services/unity-package-check.ts b/src/services/unity-package-check.ts deleted file mode 100644 index ce74365d..00000000 --- a/src/services/unity-package-check.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { DomainName } from "../domain/domain-name"; -import { checkUrlExists, CheckUrlExists } from "../io/check-url"; - -/** - * Function for checking whether a package is an official Unity package. - * @param packageName The name of the package. - * @returns A boolean indicating whether the package is a Unity package. - */ -export type CheckIsUnityPackage = (packageName: DomainName) => Promise; - -/** - * Makes a {@link CheckIsUnityPackage} function which checks whether the package - * exists by checking whether it has a doc page. - */ -export function PackageHasDocPage( - checkUrlExists: CheckUrlExists -): CheckIsUnityPackage { - return (packageName) => { - // A package is an official Unity package if it has a documentation page - const url = `https://docs.unity3d.com/Manual/${packageName}.html`; - return checkUrlExists(url); - }; -} - -/** - * Default {@link CheckIsUnityPackage}. Uses {@link PackageHasDocPage}. - */ -export const checkIsUnityPackage = PackageHasDocPage(checkUrlExists); diff --git a/src/types/another-npm-registry-client.d.ts b/src/types/another-npm-registry-client.d.ts index 9441fa2e..f8822993 100644 --- a/src/types/another-npm-registry-client.d.ts +++ b/src/types/another-npm-registry-client.d.ts @@ -1,3 +1,5 @@ +import { Logger } from "npmlog"; + declare module "another-npm-registry-client" { import { type Response } from "request"; @@ -44,6 +46,6 @@ declare module "another-npm-registry-client" { } declare module "another-npm-registry-client" { - const RegClient: new (this: Instance, config?: { log: unknown }) => Instance; + const RegClient: new (this: Instance, config?: { log?: Logger }) => Instance; export = RegClient; } diff --git a/src/types/npm-registry-fetch/lib/errors.d.ts b/src/types/npm-registry-fetch/lib/errors.d.ts deleted file mode 100644 index d0b16f1b..00000000 --- a/src/types/npm-registry-fetch/lib/errors.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {Response} from "node-fetch"; - -declare module "npm-registry-fetch/lib/errors" { - export class HttpErrorBase extends Error { - statusCode: Response["status"]; - code: `E${Response["status"]}}` | `E${string}`; - } -} diff --git a/src/utils/error-type-guards.ts b/src/utils/error-type-guards.ts index 5ccf7081..e0483e82 100644 --- a/src/utils/error-type-guards.ts +++ b/src/utils/error-type-guards.ts @@ -1,5 +1,5 @@ import { AssertionError } from "assert"; -import { HttpErrorBase } from "npm-registry-fetch/lib/errors"; +import { HttpErrorLike } from "../io/common-errors"; /** * Type guard for checking if a value is an {@link Error}. @@ -54,19 +54,19 @@ export function assertIsNodeError( } /** - * Type guard for checking whether a value is a {@link HttpErrorBase}. + * Type guard for checking whether a value is a {@link HttpErrorLike}. * @param x The value to check. */ -export function isHttpError(x: unknown): x is HttpErrorBase { +export function isHttpError(x: unknown): x is HttpErrorLike { return isError(x) && "statusCode" in x; } /** - * Asserts that a value is a {@link HttpErrorBase}. + * Asserts that a value is a {@link HttpErrorLike}. * @param x The value to assert. - * @throws AssertionError if the value is not a {@link HttpErrorBase}. + * @throws {AssertionError} If the value is not a {@link HttpErrorLike}. */ -export function assertIsHttpError(x: unknown): asserts x is HttpErrorBase { +export function assertIsHttpError(x: unknown): asserts x is HttpErrorLike { if (!isHttpError(x)) throw new AssertionError({ message: "Value is not an http error.", diff --git a/src/utils/string-utils.ts b/src/utils/string-utils.ts index 8058189c..ae1dce25 100644 --- a/src/utils/string-utils.ts +++ b/src/utils/string-utils.ts @@ -25,3 +25,12 @@ export function removeTrailingSlash(s: T): T { if (s.endsWith("/")) return s.slice(0, -1) as T; return s; } + +/** + * Splits a multiline string into it's separate lines. + * @param s The string to split. + * @returns All non-empty lines in the string. + */ +export function splitLines(s: string): ReadonlyArray { + return s.split(/[\r\n]+/).filter((it) => it.length > 0); +} diff --git a/src/utils/zod-utils.ts b/src/utils/zod-utils.ts index ce5d5d04..79e7af60 100644 --- a/src/utils/zod-utils.ts +++ b/src/utils/zod-utils.ts @@ -8,7 +8,7 @@ import { AssertionError } from "assert"; * @param schema The zod schema to check against. */ export function isZod( - value: TZod["_input"], + value: unknown, schema: TZod ): value is TZod["_output"] { return schema.safeParse(value).success; @@ -19,10 +19,10 @@ export function isZod( * zod schemas type. * @param value The value to check. * @param schema The zod schema to check against. - * @throws AssertionError if value does not match schema. + * @throws {AssertionError} If value does not match schema. */ export function assertZod( - value: TZod["_input"], + value: unknown, schema: TZod ): asserts value is TZod["_output"] { const result = schema.safeParse(value); @@ -55,7 +55,7 @@ export function removeExplicitUndefined( * * You can use this function to circumvent [issue #365](https://github.com/colinhacks/zod/issues/635). * @param value The value. - * @throws Error if the passed value is undefined. + * @throws {Error} If the passed value is undefined. */ export function removeExplicitUndefined(value: unknown) { if (value === undefined) diff --git a/test/cli/cmd-deps.test.ts b/test/cli/cmd-deps.test.ts deleted file mode 100644 index 8ca7bbd4..00000000 --- a/test/cli/cmd-deps.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { makeDepsCmd } from "../../src/cli/cmd-deps"; -import { Env, ParseEnv } from "../../src/services/parse-env"; -import { exampleRegistryUrl } from "../domain/data-registry"; -import { unityRegistryUrl } from "../../src/domain/registry-url"; -import { DomainName } from "../../src/domain/domain-name"; -import { makePackageReference } from "../../src/domain/package-reference"; -import { makeMockLogger } from "./log.mock"; -import { SemanticVersion } from "../../src/domain/semantic-version"; -import { PackumentNotFoundError } from "../../src/common-errors"; -import { ResolveDependencies } from "../../src/services/dependency-resolving"; -import { mockService } from "../services/service.mock"; -import { noopLogger } from "../../src/logging"; -import { ResultCodes } from "../../src/cli/result-codes"; -import { GetLatestVersion } from "../../src/services/get-latest-version"; -import { - makeGraphFromSeed, - markBuiltInResolved, - markRemoteResolved, -} from "../../src/domain/dependency-graph"; - -const somePackage = DomainName.parse("com.some.package"); -const otherPackage = DomainName.parse("com.other.package"); -const anotherPackage = DomainName.parse("com.another.package"); - -const defaultEnv = { - registry: { url: exampleRegistryUrl, auth: null }, - upstreamRegistry: { url: unityRegistryUrl, auth: null }, -} as Env; - -const someVersion = SemanticVersion.parse("1.2.3"); - -function makeDependencies() { - const parseEnv = mockService(); - parseEnv.mockResolvedValue(defaultEnv); - - const resolveDependencies = mockService(); - let defaultGraph = makeGraphFromSeed(somePackage, someVersion); - defaultGraph = markRemoteResolved( - defaultGraph, - somePackage, - someVersion, - exampleRegistryUrl, - { [otherPackage]: someVersion } - ); - defaultGraph = markRemoteResolved( - defaultGraph, - otherPackage, - someVersion, - exampleRegistryUrl, - {} - ); - resolveDependencies.mockResolvedValue(defaultGraph); - - const resolveLatestVersion = mockService(); - resolveLatestVersion.mockResolvedValue(someVersion); - - const log = makeMockLogger(); - - const depsCmd = makeDepsCmd( - parseEnv, - resolveDependencies, - resolveLatestVersion, - log, - noopLogger - ); - return { - depsCmd, - parseEnv, - resolveDependencies, - resolveLatestVersion, - log, - } as const; -} - -describe("cmd-deps", () => { - it("should fail if package-reference has url-version", async () => { - const { depsCmd } = makeDependencies(); - - const resultCode = await depsCmd( - makePackageReference(somePackage, "https://some.registry.com"), - {} - ); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - - it("should fail if latest version could not be r esolved", async () => { - const { depsCmd, resolveLatestVersion } = makeDependencies(); - resolveLatestVersion.mockResolvedValue(null); - - await expect(depsCmd(somePackage, {})).rejects.toBeInstanceOf( - PackumentNotFoundError - ); - }); - - it("should notify if package-reference has url-version", async () => { - const { depsCmd, log } = makeDependencies(); - - await depsCmd( - makePackageReference(somePackage, "https://some.registry.com"), - {} - ); - - expect(log.error).toHaveBeenCalledWith( - expect.any(String), - expect.stringContaining("url-version") - ); - }); - - it("should print dependency graph", async () => { - const { depsCmd, resolveDependencies, log } = makeDependencies(); - let graph = makeGraphFromSeed(somePackage, someVersion); - graph = markRemoteResolved( - graph, - somePackage, - someVersion, - exampleRegistryUrl, - { - [otherPackage]: someVersion, - [anotherPackage]: someVersion, - } - ); - graph = markBuiltInResolved(graph, otherPackage, someVersion); - graph = markBuiltInResolved(graph, anotherPackage, someVersion); - resolveDependencies.mockResolvedValue(graph); - - await depsCmd(somePackage, {}); - - // Here we just do some generic checks to see if something containing - // all relevant information was logged. For more detailed testing - // of the output format see the tests for the dependency graph - // logging logic. - expect(log.notice).toHaveBeenCalledWith( - "", - expect.stringContaining(somePackage) - ); - expect(log.notice).toHaveBeenCalledWith( - "", - expect.stringContaining(otherPackage) - ); - expect(log.notice).toHaveBeenCalledWith( - "", - expect.stringContaining(anotherPackage) - ); - }); -}); diff --git a/test/cli/cmd-login.test.ts b/test/cli/cmd-login.test.ts deleted file mode 100644 index 5de0b551..00000000 --- a/test/cli/cmd-login.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { makeLoginCmd } from "../../src/cli/cmd-login"; -import { mockService } from "../services/service.mock"; -import { Env, ParseEnv } from "../../src/services/parse-env"; -import { GetUpmConfigPath } from "../../src/io/upm-config-io"; -import { Login } from "../../src/services/login"; -import { makeMockLogger } from "./log.mock"; -import { exampleRegistryUrl } from "../domain/data-registry"; -import { unityRegistryUrl } from "../../src/domain/registry-url"; - -const defaultEnv = { - cwd: "/users/some-user/projects/SomeProject", - upstream: true, - registry: { url: exampleRegistryUrl, auth: null }, - upstreamRegistry: { url: unityRegistryUrl, auth: null }, -} as Env; -const exampleUser = "user"; -const examplePassword = "pass"; -const exampleEmail = "user@email.com"; -const exampleUpmConfigPath = "/user/home/.upmconfig.toml"; - -describe("cmd-login", () => { - function makeDependencies() { - const parseEnv = mockService(); - parseEnv.mockResolvedValue(defaultEnv); - - const getUpmConfigPath = mockService(); - getUpmConfigPath.mockResolvedValue(exampleUpmConfigPath); - - const login = mockService(); - login.mockResolvedValue(undefined); - - const log = makeMockLogger(); - - const loginCmd = makeLoginCmd(parseEnv, getUpmConfigPath, login, log); - return { loginCmd, parseEnv, getUpmConfigPath, login, log } as const; - } - - // TODO: Add tests for prompting logic - - it("should notify of success", async () => { - const { loginCmd, log } = makeDependencies(); - - await loginCmd({ - username: exampleUser, - password: examplePassword, - email: exampleEmail, - registry: exampleRegistryUrl, - }); - - expect(log.notice).toHaveBeenCalledWith( - "config", - expect.stringContaining("saved unity config") - ); - }); -}); diff --git a/test/cli/cmd-remove.test.ts b/test/cli/cmd-remove.test.ts deleted file mode 100644 index d8ab85c2..00000000 --- a/test/cli/cmd-remove.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { exampleRegistryUrl } from "../domain/data-registry"; -import { Env, ParseEnv } from "../../src/services/parse-env"; -import { makeRemoveCmd } from "../../src/cli/cmd-remove"; -import { DomainName } from "../../src/domain/domain-name"; -import { SemanticVersion } from "../../src/domain/semantic-version"; -import { makeMockLogger } from "./log.mock"; -import { mockService } from "../services/service.mock"; -import { RemovePackages } from "../../src/services/remove-packages"; -import { AsyncOk } from "../../src/utils/result-utils"; - -const somePackage = DomainName.parse("com.some.package"); -const defaultEnv = { - cwd: "/users/some-user/projects/SomeProject", - registry: { url: exampleRegistryUrl, auth: null }, -} as Env; - -function makeDependencies() { - const parseEnv = mockService(); - parseEnv.mockResolvedValue(defaultEnv); - - const removePackages = mockService(); - removePackages.mockReturnValue( - AsyncOk([{ name: somePackage, version: "1.0.0" as SemanticVersion }]) - ); - - const log = makeMockLogger(); - - const removeCmd = makeRemoveCmd(parseEnv, removePackages, log); - return { - removeCmd, - parseEnv, - removePackages, - log, - } as const; -} - -describe("cmd-remove", () => { - it("should print removed packages", async () => { - const { removeCmd, log } = makeDependencies(); - - await removeCmd([somePackage], {}); - - expect(log.notice).toHaveBeenCalledWith( - expect.any(String), - expect.stringContaining(`${somePackage}@1.0.0`) - ); - }); - - it("should suggest to open Unity after save", async () => { - const { removeCmd, log } = makeDependencies(); - - await removeCmd([somePackage], {}); - - expect(log.notice).toHaveBeenCalledWith( - "", - expect.stringContaining("open Unity") - ); - }); -}); diff --git a/test/cli/cmd-search.test.ts b/test/cli/cmd-search.test.ts deleted file mode 100644 index fc456ad0..00000000 --- a/test/cli/cmd-search.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { makeSearchCmd, SearchOptions } from "../../src/cli/cmd-search"; -import { DomainName } from "../../src/domain/domain-name"; -import { SemanticVersion } from "../../src/domain/semantic-version"; -import { makeMockLogger } from "./log.mock"; -import { SearchedPackument } from "../../src/io/npm-search"; -import { exampleRegistryUrl } from "../domain/data-registry"; -import { Env, ParseEnv } from "../../src/services/parse-env"; -import { mockService } from "../services/service.mock"; -import { SearchPackages } from "../../src/services/search-packages"; -import { noopLogger } from "../../src/logging"; -import { ResultCodes } from "../../src/cli/result-codes"; - -const exampleSearchResult: SearchedPackument = { - name: DomainName.parse("com.example.package-a"), - versions: { [SemanticVersion.parse("1.0.0")]: "latest" }, - description: "A demo package", - date: new Date(2019, 9, 2, 3, 2, 38), - "dist-tags": { latest: SemanticVersion.parse("1.0.0") }, -}; - -function makeDependencies() { - const parseEnv = mockService(); - parseEnv.mockResolvedValue({ - registry: { url: exampleRegistryUrl, auth: null }, - } as Env); - - const searchPackages = mockService(); - searchPackages.mockResolvedValue([exampleSearchResult]); - - const log = makeMockLogger(); - - const searchCmd = makeSearchCmd(parseEnv, searchPackages, log, noopLogger); - return { - searchCmd, - parseEnv, - searchPackages, - log, - } as const; -} - -describe("cmd-search", () => { - const options: SearchOptions = { - registry: exampleRegistryUrl, - upstream: false, - }; - - it("should print packument information", async () => { - const consoleSpy = jest.spyOn(console, "log"); - const { searchCmd } = makeDependencies(); - - await searchCmd("package-a", options); - - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining("package-a") - ); - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("1.0.0")); - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining("2019-10-02") - ); - }); - - it("should be ok if no network error occurred", async () => { - const { searchCmd } = makeDependencies(); - - const resultCode = await searchCmd("pkg-not-exist", options); - - expect(resultCode).toEqual(ResultCodes.Ok); - }); - - it("should notify of unknown packument", async () => { - const { searchCmd, searchPackages, log } = makeDependencies(); - searchPackages.mockResolvedValue([]); - - await searchCmd("pkg-not-exist", options); - - expect(log.notice).toHaveBeenCalledWith( - "", - expect.stringContaining("No matches found") - ); - }); - - it("should notify when falling back to old search", async () => { - const { searchCmd, searchPackages, log } = makeDependencies(); - searchPackages.mockImplementation( - async (_registry, _keyword, onUseAllFallback) => { - onUseAllFallback && onUseAllFallback(); - return []; - } - ); - - await searchCmd("package-a", options); - - expect(log.warn).toHaveBeenCalledWith( - "", - expect.stringContaining("using old search") - ); - }); -}); diff --git a/test/cli/cmd-view.test.ts b/test/cli/cmd-view.test.ts deleted file mode 100644 index 66061b6f..00000000 --- a/test/cli/cmd-view.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { makeViewCmd } from "../../src/cli/cmd-view"; -import { Env, ParseEnv } from "../../src/services/parse-env"; -import { exampleRegistryUrl } from "../domain/data-registry"; -import { unityRegistryUrl } from "../../src/domain/registry-url"; -import { DomainName } from "../../src/domain/domain-name"; -import { makePackageReference } from "../../src/domain/package-reference"; -import { SemanticVersion } from "../../src/domain/semantic-version"; -import { makeMockLogger } from "./log.mock"; -import { buildPackument } from "../domain/data-packument"; -import { mockService } from "../services/service.mock"; -import { ResultCodes } from "../../src/cli/result-codes"; -import { GetRegistryPackument } from "../../src/io/packument-io"; -import { PackumentNotFoundError } from "../../src/common-errors"; - -const somePackage = DomainName.parse("com.some.package"); -const somePackument = buildPackument(somePackage, (packument) => - packument - .set("time", { - modified: "2019-11-28T18:51:58.123Z", - created: "2019-11-28T18:51:58.123Z", - [SemanticVersion.parse("1.0.0")]: "2019-11-28T18:51:58.123Z", - }) - .addVersion("1.0.0", (version) => - version - .set("displayName", "Package A") - .set("author", { name: "batman" }) - .set("unity", "2018.4") - .set("description", "A demo package") - .set("keywords", [""]) - .set("dist", { - integrity: - "sha512-MAh44bur7HGyfbCXH9WKfaUNS67aRMfO0VAbLkr+jwseb1hJue/I1pKsC7PKksuBYh4oqoo9Jov1cBcvjVgjmA==", - shasum: "516957cac4249f95cafab0290335def7d9703db7", - tarball: - "https://cdn.example.com/com.example.package-a/com.example.package-a-1.0.0.tgz", - }) - ) -); -const defaultEnv = { - upstream: false, - registry: { url: exampleRegistryUrl, auth: null }, - upstreamRegistry: { url: unityRegistryUrl, auth: null }, -} as Env; - -function makeDependencies() { - const parseEnv = mockService(); - parseEnv.mockResolvedValue(defaultEnv); - - const getRegistryPackument = mockService(); - getRegistryPackument.mockResolvedValue(somePackument); - - const log = makeMockLogger(); - - const viewCmd = makeViewCmd(parseEnv, getRegistryPackument, log); - return { - viewCmd, - parseEnv, - getRegistryPackument: getRegistryPackument, - log, - } as const; -} - -describe("cmd-view", () => { - it("should fail if package version was specified", async () => { - const { viewCmd } = makeDependencies(); - - const resultCode = await viewCmd( - makePackageReference(somePackage, SemanticVersion.parse("1.0.0")), - {} - ); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - - it("should fail if package is not found", async () => { - const { viewCmd, getRegistryPackument } = makeDependencies(); - getRegistryPackument.mockResolvedValue(null); - - await expect(viewCmd(somePackage, {})).rejects.toBeInstanceOf( - PackumentNotFoundError - ); - }); - - it("should notify if package version was specified", async () => { - const { viewCmd, log } = makeDependencies(); - - await viewCmd( - makePackageReference(somePackage, SemanticVersion.parse("1.0.0")), - {} - ); - - expect(log.warn).toHaveBeenCalledWith( - "", - expect.stringContaining("please do not specify") - ); - }); - - it("should print package information", async () => { - const consoleSpy = jest.spyOn(console, "log"); - const { viewCmd } = makeDependencies(); - - await viewCmd(somePackage, {}); - - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining(somePackage) - ); - }); -}); diff --git a/test/domain/upm-config.test.ts b/test/domain/upm-config.test.ts deleted file mode 100644 index 661294cc..00000000 --- a/test/domain/upm-config.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { tryGetAuthForRegistry, UpmConfig } from "../../src/domain/upm-config"; -import { NpmAuth } from "another-npm-registry-client"; - -import { exampleRegistryUrl } from "./data-registry"; - -describe("upm-config", () => { - const someAuth: NpmAuth = { - username: "user", - password: "pass", - email: "email@wow.com", - }; - - describe("get auth for registry", () => { - it("should find auth that was added", () => { - const config: UpmConfig = { - [exampleRegistryUrl]: someAuth, - }; - - const actual = tryGetAuthForRegistry(config, exampleRegistryUrl); - - expect(actual).toEqual(someAuth); - }); - - it("should not find auth for url that does not exist", () => { - const config: UpmConfig = {}; - - const actual = tryGetAuthForRegistry(config, exampleRegistryUrl); - - expect(actual).toBeNull(); - }); - }); -}); diff --git a/test/e2e/add.e2e.ts b/test/e2e/add.test.ts similarity index 100% rename from test/e2e/add.e2e.ts rename to test/e2e/add.test.ts diff --git a/test/e2e/deps.e2e.ts b/test/e2e/deps.test.ts similarity index 100% rename from test/e2e/deps.e2e.ts rename to test/e2e/deps.test.ts diff --git a/test/e2e/help.e2e.ts b/test/e2e/help.test.ts similarity index 100% rename from test/e2e/help.e2e.ts rename to test/e2e/help.test.ts diff --git a/test/e2e/remove.test.ts b/test/e2e/remove.test.ts new file mode 100644 index 00000000..85497550 --- /dev/null +++ b/test/e2e/remove.test.ts @@ -0,0 +1,126 @@ +import { ResultCodes } from "../../src/cli/result-codes"; +import { emptyProjectManifest } from "../../src/domain/project-manifest"; +import { buildProjectManifest } from "../unit/domain/data-project-manifest"; +import { exampleRegistryUrl } from "../unit/domain/data-registry"; +import { getProjectManifest } from "./check/project-manifest"; +import { runOpenupm } from "./run"; +import { prepareHomeDirectory } from "./setup/directories"; +import { prepareUnityProject } from "./setup/project"; + +describe("remove packages", () => { + it("should not accept package reference with version", async () => { + const homeDirectory = await prepareHomeDirectory(); + const projectDirectory = await prepareUnityProject(homeDirectory); + + const result = await runOpenupm(projectDirectory, [ + "remove", + "dev.comradevanti.opt-unity@2.0.0", + ]); + + expect(result.code).toEqual(ResultCodes.Error); + }); + + it("should remove package from manifest (manifest is empty afterwards)", async () => { + const homeDirectory = await prepareHomeDirectory(); + const projectDirectory = await prepareUnityProject(homeDirectory, { + manifest: buildProjectManifest((manifest) => + manifest.addDependency( + "dev.comradevanti.opt-unity", + "2.0.0", + true, + true + ) + ), + }); + + const result = await runOpenupm(projectDirectory, [ + "remove", + "dev.comradevanti.opt-unity", + ]); + + const actualManifest = await getProjectManifest(projectDirectory); + expect(result.code).toEqual(ResultCodes.Ok); + expect(result.stdErr).toEqual( + expect.arrayContaining([ + expect.stringContaining('Removed "dev.comradevanti.opt-unity@2.0.0"'), + expect.stringContaining("please open Unity"), + ]) + ); + expect(actualManifest).toEqual(emptyProjectManifest); + }); + + it("should remove package from manifest (other packages remain)", async () => { + const homeDirectory = await prepareHomeDirectory(); + const projectDirectory = await prepareUnityProject(homeDirectory, { + manifest: buildProjectManifest((manifest) => + manifest + .addDependency("dev.comradevanti.opt-unity", "2.0.0", true, true) + .addDependency("some.other.package", "1.0.0", true, false) + ), + }); + + const result = await runOpenupm(projectDirectory, [ + "remove", + "dev.comradevanti.opt-unity", + ]); + + const actualManifest = await getProjectManifest(projectDirectory); + expect(result.code).toEqual(ResultCodes.Ok); + expect(result.stdErr).toEqual( + expect.arrayContaining([ + expect.stringContaining('Removed "dev.comradevanti.opt-unity@2.0.0"'), + expect.stringContaining("please open Unity"), + ]) + ); + expect(actualManifest).toEqual({ + dependencies: { "some.other.package": "1.0.0" }, + scopedRegistries: [ + { + name: "example.com", + url: exampleRegistryUrl, + scopes: ["some.other.package"], + }, + ], + }); + }); + + it("should fail if package is not in manifest", async () => { + const homeDirectory = await prepareHomeDirectory(); + const projectDirectory = await prepareUnityProject(homeDirectory); + + const result = await runOpenupm(projectDirectory, [ + "remove", + "dev.comradevanti.opt-unity", + ]); + + expect(result.code).toEqual(ResultCodes.Error); + expect(result.stdErr).toEqual( + expect.arrayContaining([ + expect.stringContaining( + 'Package "dev.comradevanti.opt-unity" could not be found' + ), + expect.stringContaining("Did you make a typo"), + ]) + ); + }); + + it("should be atomic", async () => { + const homeDirectory = await prepareHomeDirectory(); + const initialManifest = buildProjectManifest((manifest) => + manifest.addDependency("dev.comradevanti.opt-unity", "2.0.0", true, true) + ); + const projectDirectory = await prepareUnityProject(homeDirectory, { + manifest: initialManifest, + }); + + const result = await runOpenupm(projectDirectory, [ + "remove", + "dev.comradevanti.opt-unity", + "other.unknown.package", + ]); + + const actualManifest = await getProjectManifest(projectDirectory); + expect(result.code).toEqual(ResultCodes.Error); + expect(actualManifest).toEqual(initialManifest); + }); +}); diff --git a/test/e2e/search.test.ts b/test/e2e/search.test.ts new file mode 100644 index 00000000..09a98ac6 --- /dev/null +++ b/test/e2e/search.test.ts @@ -0,0 +1,32 @@ +import { ResultCodes } from "../../src/cli/result-codes"; +import { runOpenupm } from "./run"; +import { prepareHomeDirectory } from "./setup/directories"; + +describe("cmd-search", () => { + it("should print packument information", async () => { + const homeDir = await prepareHomeDirectory(); + const output = await runOpenupm(homeDir, [ + "search", + "comradevanti.opt-unity", + ]); + + expect(output.code).toEqual(ResultCodes.Ok); + expect(output.stdErr).toEqual( + expect.arrayContaining([ + expect.stringContaining("dev.comradevanti.opt-unity"), + expect.stringContaining("3.5.0"), + expect.stringContaining("2023-02-28"), + ]) + ); + }); + + it("should notify of unknown packument", async () => { + const homeDir = await prepareHomeDirectory(); + const output = await runOpenupm(homeDir, ["search", "pkg-not-exist"]); + + expect(output.code).toEqual(ResultCodes.Ok); + expect(output.stdErr).toEqual( + expect.arrayContaining([expect.stringContaining("No matches found")]) + ); + }); +}); diff --git a/test/e2e/setup/project.ts b/test/e2e/setup/project.ts index cd921864..1cd53e85 100644 --- a/test/e2e/setup/project.ts +++ b/test/e2e/setup/project.ts @@ -1,7 +1,10 @@ import fse from "fs-extra"; import path from "path"; import yaml from "yaml"; -import { emptyProjectManifest } from "../../../src/domain/project-manifest"; +import { + emptyProjectManifest, + UnityProjectManifest, +} from "../../../src/domain/project-manifest"; import { WriteProjectManifestFile } from "../../../src/io/project-manifest-io"; import { projectVersionTxtPathFor } from "../../../src/io/project-version-io"; import { writeTextFile } from "../../../src/io/text-file-io"; @@ -16,7 +19,8 @@ const writeProjectManifest = WriteProjectManifestFile(writeTextFile); * @returns The path to the created project's directory. */ export async function prepareUnityProject( - homeDirectory: string + homeDirectory: string, + options?: { manifest?: UnityProjectManifest } ): Promise { const projectName = "MyProject"; const projectDir = path.join(homeDirectory, projectName); @@ -32,7 +36,10 @@ export async function prepareUnityProject( { encoding: "utf8" } ); - await writeProjectManifest(projectDir, emptyProjectManifest); + await writeProjectManifest( + projectDir, + options?.manifest ?? emptyProjectManifest + ); return projectDir; } diff --git a/test/e2e/unknown.e2e.ts b/test/e2e/unknown.test.ts similarity index 100% rename from test/e2e/unknown.e2e.ts rename to test/e2e/unknown.test.ts diff --git a/test/e2e/version.e2e.ts b/test/e2e/version.test.ts similarity index 100% rename from test/e2e/version.e2e.ts rename to test/e2e/version.test.ts diff --git a/test/e2e/view.test.ts b/test/e2e/view.test.ts new file mode 100644 index 00000000..56d1610b --- /dev/null +++ b/test/e2e/view.test.ts @@ -0,0 +1,55 @@ +import { ResultCodes } from "../../src/cli/result-codes"; +import { runOpenupm } from "./run"; +import { prepareHomeDirectory } from "./setup/directories"; + +describe("view packages", () => { + it("should fail if package version was specified", async () => { + const homeDir = await prepareHomeDirectory(); + + const output = await runOpenupm(homeDir, [ + "view", + "dev.comradevanti.opt-unity@2.0.0", + ]); + + expect(output.code).toEqual(ResultCodes.Error); + expect(output.stdErr).toEqual( + expect.arrayContaining([ + expect.stringContaining("please do not specify a version"), + ]) + ); + }); + + it("should fail if package is not found", async () => { + const homeDir = await prepareHomeDirectory(); + + const output = await runOpenupm(homeDir, [ + "view", + "not.existent.package.123456", + ]); + + expect(output.code).toEqual(ResultCodes.Error); + expect(output.stdErr).toEqual( + expect.arrayContaining([expect.stringContaining("could not be found")]) + ); + }); + + it("should print package information", async () => { + const homeDir = await prepareHomeDirectory(); + + const output = await runOpenupm(homeDir, [ + "view", + "dev.comradevanti.opt-unity", + // We need to disable color, otherwise chalk will mess with the output + "--no-color" + ]); + + expect(output.code).toEqual(ResultCodes.Ok); + expect(output.stdErr).toEqual( + expect.arrayContaining([ + expect.stringContaining( + "dev.comradevanti.opt-unity@3.5.0 | Unlicense | versions: 10" + ), + ]) + ); + }); +}); diff --git a/test/io/check-url.test.ts b/test/io/check-url.test.ts deleted file mode 100644 index a8a20abf..00000000 --- a/test/io/check-url.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import fetch, { Response } from "node-fetch"; -import { CheckUrlIsOk } from "../../src/io/check-url"; - -jest.mock("node-fetch"); - -describe("check url is ok", () => { - function makeDependencies() { - const checkUrlIsOk = CheckUrlIsOk(); - return { checkUrlIsOk } as const; - } - - it("should be true if url responds with 200", async () => { - jest.mocked(fetch).mockResolvedValue({ - status: 200, - } as Response); - const { checkUrlIsOk } = makeDependencies(); - - const actual = await checkUrlIsOk("https://some.url.com"); - - expect(actual).toBeTruthy(); - }); - - it.each([100, 201, 301, 401, 404, 500])( - "should be false other status codes (%d)", - async (statusCode) => { - jest.mocked(fetch).mockResolvedValue({ - status: statusCode, - } as Response); - const { checkUrlIsOk } = makeDependencies(); - - const actual = await checkUrlIsOk("https://some.url.com"); - - expect(actual).toBeFalsy(); - } - ); -}); diff --git a/test/io/project-manifest-io.test.ts b/test/io/project-manifest-io.test.ts deleted file mode 100644 index cedbcd8c..00000000 --- a/test/io/project-manifest-io.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { - ReadProjectManifestFile, - WriteProjectManifestFile, - ManifestMalformedError, - ManifestMissingError, - manifestPathFor, -} from "../../src/io/project-manifest-io"; -import { mapScopedRegistry } from "../../src/domain/project-manifest"; -import path from "path"; -import { ReadTextFile, WriteTextFile } from "../../src/io/text-file-io"; -import { buildProjectManifest } from "../domain/data-project-manifest"; -import { DomainName } from "../../src/domain/domain-name"; -import { removeScope } from "../../src/domain/scoped-registry"; -import { exampleRegistryUrl } from "../domain/data-registry"; -import { mockService } from "../services/service.mock"; -import { noopLogger } from "../../src/logging"; - -const exampleProjectPath = "/some/path"; -describe("project-manifest io", () => { - describe("path", () => { - it("should determine correct manifest path", () => { - const manifestPath = manifestPathFor("test-openupm-cli"); - const expected = path.join( - "test-openupm-cli", - "Packages", - "manifest.json" - ); - expect(manifestPath).toEqual(expected); - }); - }); - - describe("read file", () => { - function makeDependencies() { - const readFile = mockService(); - - const readProjectManifestFile = ReadProjectManifestFile( - readFile, - noopLogger - ); - return { readProjectManifestFile, readFile } as const; - } - - it("should fail if file is missing", async () => { - const { readProjectManifestFile, readFile } = makeDependencies(); - readFile.mockResolvedValue(null); - - await expect( - readProjectManifestFile(exampleProjectPath) - ).rejects.toBeInstanceOf(ManifestMissingError); - }); - - it("should fail if manifest contains invalid json", async () => { - const { readProjectManifestFile, readFile } = makeDependencies(); - readFile.mockResolvedValue("not {} valid : json"); - - await expect( - readProjectManifestFile(exampleProjectPath) - ).rejects.toBeInstanceOf(ManifestMalformedError); - }); - - it("should fail if manifest contains invalid content", async () => { - const { readProjectManifestFile, readFile } = makeDependencies(); - readFile.mockResolvedValue(`123`); - - await expect( - readProjectManifestFile(exampleProjectPath) - ).rejects.toBeInstanceOf(ManifestMalformedError); - }); - - it("should load valid manifest", async () => { - const { readProjectManifestFile, readFile } = makeDependencies(); - readFile.mockResolvedValue( - `{ "dependencies": { "com.package.a": "1.0.0"} }` - ); - - const actual = await readProjectManifestFile(exampleProjectPath); - - expect(actual).toEqual({ - dependencies: { - "com.package.a": "1.0.0", - }, - }); - }); - }); - - describe("write file", () => { - function makeDependencies() { - const writeFile = mockService(); - writeFile.mockResolvedValue(undefined); - - const writeProjectManifest = WriteProjectManifestFile(writeFile); - return { writeProjectManifest, writeFile } as const; - } - - it("should write manifest json", async () => { - const { writeProjectManifest, writeFile } = makeDependencies(); - - const manifest = buildProjectManifest((manifest) => - manifest.addDependency("com.package.a", "1.0.0", true, true) - ); - - await writeProjectManifest(exampleProjectPath, manifest); - - expect(writeFile).toHaveBeenCalledWith( - expect.any(String), - JSON.stringify( - { - dependencies: { - "com.package.a": "1.0.0", - }, - scopedRegistries: [ - { - name: "example.com", - url: exampleRegistryUrl, - scopes: ["com.package.a"], - }, - ], - testables: ["com.package.a"], - }, - null, - 2 - ) - ); - }); - - it("should prune manifest before writing", async () => { - const { writeProjectManifest, writeFile } = makeDependencies(); - // Add and then remove a scope to force an empty scoped-registry - const testDomain = "test" as DomainName; - let manifest = buildProjectManifest((manifest) => - manifest.addScope(testDomain) - ); - manifest = mapScopedRegistry(manifest, exampleRegistryUrl, (registry) => { - return removeScope(registry!, testDomain); - }); - - await writeProjectManifest(exampleProjectPath, manifest); - - expect(writeFile).toHaveBeenCalledWith( - expect.any(String), - JSON.stringify({ dependencies: {}, scopedRegistries: [] }, null, 2) - ); - }); - }); -}); diff --git a/test/domain/project-manifest-assertions.ts b/test/project-manifest-assertions.ts similarity index 85% rename from test/domain/project-manifest-assertions.ts rename to test/project-manifest-assertions.ts index aca0e1b3..92d113ed 100644 --- a/test/domain/project-manifest-assertions.ts +++ b/test/project-manifest-assertions.ts @@ -1,7 +1,7 @@ -import { DomainName } from "../../src/domain/domain-name"; -import { SemanticVersion } from "../../src/domain/semantic-version"; -import { PackageUrl } from "../../src/domain/package-url"; -import { UnityProjectManifest } from "../../src/domain/project-manifest"; +import { DomainName } from "../src/domain/domain-name"; +import { SemanticVersion } from "../src/domain/semantic-version"; +import { PackageUrl } from "../src/domain/package-url"; +import { UnityProjectManifest } from "../src/domain/project-manifest"; expect.extend({ toHaveDependency( diff --git a/test/services/get-registry-auth.test.ts b/test/services/get-registry-auth.test.ts deleted file mode 100644 index 1834bef0..00000000 --- a/test/services/get-registry-auth.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Base64 } from "../../src/domain/base64"; -import { emptyUpmConfig } from "../../src/domain/upm-config"; -import { LoadUpmConfig } from "../../src/io/upm-config-io"; -import { LoadRegistryAuthFromUpmConfig } from "../../src/services/get-registry-auth"; -import { exampleRegistryUrl } from "../domain/data-registry"; -import { mockService } from "./service.mock"; - -describe("get registry auth from upm config", () => { - const someConfigPath = "/home/user/.upmconfig.toml"; - const someEmail = "user@mail.com"; - const someToken = "isehusehgusheguszg8gshg"; - - function makeDependencies() { - const loadUpmConfig = mockService(); - - const loadRegistryAuthFromUpmConfig = - LoadRegistryAuthFromUpmConfig(loadUpmConfig); - return { loadRegistryAuthFromUpmConfig, loadUpmConfig } as const; - } - - it("should be empty if there is no upm config", async () => { - const { loadRegistryAuthFromUpmConfig, loadUpmConfig } = makeDependencies(); - loadUpmConfig.mockResolvedValue(null); - - const actual = await loadRegistryAuthFromUpmConfig(someConfigPath); - - expect(actual).toEqual(emptyUpmConfig); - }); - - it("should import empty", async () => { - const { loadRegistryAuthFromUpmConfig, loadUpmConfig } = makeDependencies(); - loadUpmConfig.mockResolvedValue({}); - - const actual = await loadRegistryAuthFromUpmConfig(someConfigPath); - - expect(actual).toEqual(emptyUpmConfig); - }); - - it("should remove trailing slash on registry urls", async () => { - const { loadRegistryAuthFromUpmConfig, loadUpmConfig } = makeDependencies(); - loadUpmConfig.mockResolvedValue({ - npmAuth: { - [exampleRegistryUrl + "/"]: { - _auth: "dXNlcjpwYXNz" as Base64, // user:pass - email: someEmail, - alwaysAuth: true, - }, - }, - }); - - const actual = await loadRegistryAuthFromUpmConfig(someConfigPath); - - expect(actual).toEqual({ - [exampleRegistryUrl]: { - username: "user", - password: "pass", - email: someEmail, - alwaysAuth: true, - }, - }); - }); - - it("should import valid basic auth", async () => { - const { loadRegistryAuthFromUpmConfig, loadUpmConfig } = makeDependencies(); - loadUpmConfig.mockResolvedValue({ - npmAuth: { - [exampleRegistryUrl]: { - _auth: "dXNlcjpwYXNz" as Base64, // user:pass - email: someEmail, - alwaysAuth: true, - }, - }, - }); - - const actual = await loadRegistryAuthFromUpmConfig(someConfigPath); - - expect(actual).toEqual({ - [exampleRegistryUrl]: { - username: "user", - password: "pass", - email: someEmail, - alwaysAuth: true, - }, - }); - }); - - it("should import valid token auth", async () => { - const { loadRegistryAuthFromUpmConfig, loadUpmConfig } = makeDependencies(); - loadUpmConfig.mockResolvedValue({ - npmAuth: { [exampleRegistryUrl]: { token: someToken, alwaysAuth: true } }, - }); - - const actual = await loadRegistryAuthFromUpmConfig(someConfigPath); - - expect(actual).toEqual({ - [exampleRegistryUrl]: { - token: someToken, - alwaysAuth: true, - }, - }); - }); - - it("should ignore email when importing token auth", async () => { - const { loadRegistryAuthFromUpmConfig, loadUpmConfig } = makeDependencies(); - loadUpmConfig.mockResolvedValue({ - npmAuth: { - [exampleRegistryUrl]: { - token: someToken, - email: someEmail, - }, - }, - }); - - const actual = await loadRegistryAuthFromUpmConfig(someConfigPath); - - expect(actual).toEqual({ - [exampleRegistryUrl]: { - token: someToken, - }, - }); - }); -}); diff --git a/test/services/service.mock.ts b/test/services/service.mock.ts deleted file mode 100644 index 8c74c7bb..00000000 --- a/test/services/service.mock.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Creates a mock for a service function. - * This is just a typing-utility function. Under the hood this just calls - * {@link jest.fn}. - */ -export function mockService< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends (...args: any[]) => any ->(): jest.MockedFunction { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return jest.fn(); -} diff --git a/test/services/unity-package-check.test.ts b/test/services/unity-package-check.test.ts deleted file mode 100644 index 2889d096..00000000 --- a/test/services/unity-package-check.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { DomainName } from "../../src/domain/domain-name"; -import { CheckUrlExists } from "../../src/io/check-url"; -import { PackageHasDocPage } from "../../src/services/unity-package-check"; -import { mockService } from "./service.mock"; - -describe("unity package has doc page", () => { - const somePackage = DomainName.parse("com.some.package"); - - function makeDependencies() { - const checkUrlExists = mockService(); - - const packageHasDocPage = PackageHasDocPage(checkUrlExists); - return { packageHasDocPage, checkUrlExists } as const; - } - - it("should be true if manual page exists", async () => { - const { packageHasDocPage, checkUrlExists } = makeDependencies(); - checkUrlExists.mockResolvedValue(true); - - const actual = await packageHasDocPage(somePackage); - - expect(actual).toBeTruthy(); - }); - - it("should be false if manual page does not exist", async () => { - const { packageHasDocPage, checkUrlExists } = makeDependencies(); - checkUrlExists.mockResolvedValue(false); - - const actual = await packageHasDocPage(somePackage); - - expect(actual).toBeFalsy(); - }); -}); diff --git a/test/cli/cmd-add.test.ts b/test/unit/cli/cmd-add.test.ts similarity index 85% rename from test/cli/cmd-add.test.ts rename to test/unit/cli/cmd-add.test.ts index cb655da6..bae31b63 100644 --- a/test/cli/cmd-add.test.ts +++ b/test/unit/cli/cmd-add.test.ts @@ -3,38 +3,39 @@ import { makeAddCmd, PackageIncompatibleError, UnresolvedDependenciesError, -} from "../../src/cli/cmd-add"; -import { ResultCodes } from "../../src/cli/result-codes"; -import { PackumentNotFoundError } from "../../src/common-errors"; +} from "../../../src/cli/cmd-add"; +import { ResultCodes } from "../../../src/cli/result-codes"; +import { PackumentNotFoundError } from "../../../src/common-errors"; import { makeGraphFromSeed, markFailed, markRemoteResolved, -} from "../../src/domain/dependency-graph"; -import { DomainName } from "../../src/domain/domain-name"; -import { makeEditorVersion } from "../../src/domain/editor-version"; -import { UnityPackumentVersion } from "../../src/domain/packument"; -import { emptyProjectManifest } from "../../src/domain/project-manifest"; -import { unityRegistryUrl } from "../../src/domain/registry-url"; -import { SemanticVersion } from "../../src/domain/semantic-version"; +} from "../../../src/domain/dependency-graph"; +import { DomainName } from "../../../src/domain/domain-name"; +import { makeEditorVersion } from "../../../src/domain/editor-version"; +import { UnityPackumentVersion } from "../../../src/domain/packument"; +import { emptyProjectManifest } from "../../../src/domain/project-manifest"; +import { unityRegistryUrl } from "../../../src/domain/registry-url"; +import { SemanticVersion } from "../../../src/domain/semantic-version"; import { LoadProjectManifest, SaveProjectManifest, -} from "../../src/io/project-manifest-io"; -import { noopLogger } from "../../src/logging"; -import { ResolveDependencies } from "../../src/services/dependency-resolving"; -import { DetermineEditorVersion } from "../../src/services/determine-editor-version"; +} from "../../../src/io/project-manifest-io"; +import { noopLogger } from "../../../src/logging"; +import { ResolveDependencies } from "../../../src/services/dependency-resolving"; +import { DetermineEditorVersion } from "../../../src/services/determine-editor-version"; +import { GetRegistryAuth } from "../../../src/services/get-registry-auth"; import { GetRegistryPackumentVersion, ResolvedPackumentVersion, -} from "../../src/services/get-registry-packument-version"; -import { Env, ParseEnv } from "../../src/services/parse-env"; -import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; +} from "../../../src/services/get-registry-packument-version"; +import { Env, ParseEnv } from "../../../src/services/parse-env"; +import { AsyncErr, AsyncOk } from "../../../src/utils/result-utils"; import { buildPackument } from "../domain/data-packument"; import { buildProjectManifest } from "../domain/data-project-manifest"; import { exampleRegistryUrl } from "../domain/data-registry"; import { mockResolvedPackuments } from "../services/remote-packuments.mock"; -import { mockService } from "../services/service.mock"; +import { mockFunctionOfType } from "../services/func.mock"; import { makeMockLogger } from "./log.mock"; const somePackage = DomainName.parse("com.some.package"); @@ -61,25 +62,24 @@ const incompatiblePackument = buildPackument(somePackage, (packument) => const defaultEnv = { cwd: "/users/some-user/projects/SomeProject", upstream: true, - registry: { url: exampleRegistryUrl, auth: null }, - upstreamRegistry: { url: unityRegistryUrl, auth: null }, + primaryRegistryUrl: exampleRegistryUrl, } as Env; const someVersion = SemanticVersion.parse("1.0.0"); function makeDependencies() { - const parseEnv = mockService(); + const parseEnv = mockFunctionOfType(); parseEnv.mockResolvedValue(defaultEnv); const getRegistryPackumentVersion = - mockService(); + mockFunctionOfType(); mockResolvedPackuments( getRegistryPackumentVersion, [exampleRegistryUrl, somePackument], [exampleRegistryUrl, otherPackument] ); - const resolveDependencies = mockService(); + const resolveDependencies = mockFunctionOfType(); let defaultGraph = makeGraphFromSeed(somePackage, someVersion); defaultGraph = markRemoteResolved( defaultGraph, @@ -97,17 +97,20 @@ function makeDependencies() { ); resolveDependencies.mockResolvedValue(defaultGraph); - const loadProjectManifest = mockService(); + const loadProjectManifest = mockFunctionOfType(); loadProjectManifest.mockResolvedValue(emptyProjectManifest); - const writeProjectManifest = mockService(); + const writeProjectManifest = mockFunctionOfType(); writeProjectManifest.mockResolvedValue(undefined); - const determineEditorVersion = mockService(); + const determineEditorVersion = mockFunctionOfType(); determineEditorVersion.mockResolvedValue( makeEditorVersion(2022, 2, 1, "f", 2) ); + const getRegistryAuth = mockFunctionOfType(); + getRegistryAuth.mockResolvedValue({ url: exampleRegistryUrl, auth: null }); + const log = makeMockLogger(); const addCmd = makeAddCmd( @@ -117,6 +120,7 @@ function makeDependencies() { loadProjectManifest, writeProjectManifest, determineEditorVersion, + getRegistryAuth, log, noopLogger ); diff --git a/test/cli/dependency-logging.test.ts b/test/unit/cli/dependency-logging.test.ts similarity index 91% rename from test/cli/dependency-logging.test.ts rename to test/unit/cli/dependency-logging.test.ts index 23b9bd93..c04b23c4 100644 --- a/test/cli/dependency-logging.test.ts +++ b/test/unit/cli/dependency-logging.test.ts @@ -3,15 +3,15 @@ import { markBuiltInResolved, markFailed, markRemoteResolved, -} from "../../src/domain/dependency-graph"; -import { stringifyDependencyGraph } from "../../src/cli/dependency-logging"; -import { makePackageReference } from "../../src/domain/package-reference"; +} from "../../../src/domain/dependency-graph"; +import { stringifyDependencyGraph } from "../../../src/cli/dependency-logging"; +import { makePackageReference } from "../../../src/domain/package-reference"; import { exampleRegistryUrl } from "../domain/data-registry"; -import { PackumentNotFoundError } from "../../src/common-errors"; -import { unityRegistryUrl } from "../../src/domain/registry-url"; -import { VersionNotFoundError } from "../../src/domain/packument"; -import { DomainName } from "../../src/domain/domain-name"; -import { SemanticVersion } from "../../src/domain/semantic-version"; +import { PackumentNotFoundError } from "../../../src/common-errors"; +import { unityRegistryUrl } from "../../../src/domain/registry-url"; +import { VersionNotFoundError } from "../../../src/domain/packument"; +import { DomainName } from "../../../src/domain/domain-name"; +import { SemanticVersion } from "../../../src/domain/semantic-version"; describe("dependency-logging", () => { describe("graph", () => { diff --git a/test/cli/log.mock.ts b/test/unit/cli/log.mock.ts similarity index 100% rename from test/cli/log.mock.ts rename to test/unit/cli/log.mock.ts diff --git a/test/domain/base64.test.ts b/test/unit/domain/base64.test.ts similarity index 81% rename from test/domain/base64.test.ts rename to test/unit/domain/base64.test.ts index 39f57672..a98d7200 100644 --- a/test/domain/base64.test.ts +++ b/test/unit/domain/base64.test.ts @@ -1,4 +1,4 @@ -import { decodeBase64, encodeBase64 } from "../../src/domain/base64"; +import { decodeBase64, encodeBase64 } from "../../../src/domain/base64"; import fc from "fast-check"; describe("base 64", () => { diff --git a/test/domain/data-packument.ts b/test/unit/domain/data-packument.ts similarity index 92% rename from test/domain/data-packument.ts rename to test/unit/domain/data-packument.ts index 1823b5ed..f402e868 100644 --- a/test/domain/data-packument.ts +++ b/test/unit/domain/data-packument.ts @@ -1,11 +1,11 @@ import assert from "assert"; -import { DomainName } from "../../src/domain/domain-name"; -import { SemanticVersion } from "../../src/domain/semantic-version"; +import { DomainName } from "../../../src/domain/domain-name"; +import { SemanticVersion } from "../../../src/domain/semantic-version"; import { UnityPackument, UnityPackumentVersion, -} from "../../src/domain/packument"; -import { isZod } from "../../src/utils/zod-utils"; +} from "../../../src/domain/packument"; +import { isZod } from "../../../src/utils/zod-utils"; /** * Builder class for {@link UnityPackumentVersion}. diff --git a/test/domain/data-project-manifest.ts b/test/unit/domain/data-project-manifest.ts similarity index 87% rename from test/domain/data-project-manifest.ts rename to test/unit/domain/data-project-manifest.ts index 0e515cc4..1dcdd1e7 100644 --- a/test/domain/data-project-manifest.ts +++ b/test/unit/domain/data-project-manifest.ts @@ -1,15 +1,15 @@ -import { SemanticVersion } from "../../src/domain/semantic-version"; -import { addScope, makeScopedRegistry } from "../../src/domain/scoped-registry"; +import { SemanticVersion } from "../../../src/domain/semantic-version"; +import { addScope, makeScopedRegistry } from "../../../src/domain/scoped-registry"; import { addTestable, emptyProjectManifest, mapScopedRegistry, setDependency, UnityProjectManifest, -} from "../../src/domain/project-manifest"; +} from "../../../src/domain/project-manifest"; import { exampleRegistryUrl } from "./data-registry"; -import { assertZod } from "../../src/utils/zod-utils"; -import { DomainName } from "../../src/domain/domain-name"; +import { assertZod } from "../../../src/utils/zod-utils"; +import { DomainName } from "../../../src/domain/domain-name"; /** * Builder class for {@link UnityProjectManifest}. diff --git a/test/domain/data-registry.ts b/test/unit/domain/data-registry.ts similarity index 54% rename from test/domain/data-registry.ts rename to test/unit/domain/data-registry.ts index a2e0581c..209f9669 100644 --- a/test/domain/data-registry.ts +++ b/test/unit/domain/data-registry.ts @@ -1,3 +1,3 @@ -import { RegistryUrl } from "../../src/domain/registry-url"; +import { RegistryUrl } from "../../../src/domain/registry-url"; export const exampleRegistryUrl = RegistryUrl.parse("https://example.com"); diff --git a/test/domain/dependency-graph.test.ts b/test/unit/domain/dependency-graph.test.ts similarity index 95% rename from test/domain/dependency-graph.test.ts rename to test/unit/domain/dependency-graph.test.ts index ecb8f0fa..e694ef4d 100644 --- a/test/domain/dependency-graph.test.ts +++ b/test/unit/domain/dependency-graph.test.ts @@ -1,5 +1,5 @@ -import { DomainName } from "../../src/domain/domain-name"; -import { SemanticVersion } from "../../src/domain/semantic-version"; +import { DomainName } from "../../../src/domain/domain-name"; +import { SemanticVersion } from "../../../src/domain/semantic-version"; import { graphNodeCount, makeGraphFromSeed, @@ -10,9 +10,9 @@ import { traverseDependencyGraph, tryGetGraphNode, tryGetNextUnresolved, -} from "../../src/domain/dependency-graph"; +} from "../../../src/domain/dependency-graph"; import { exampleRegistryUrl } from "./data-registry"; -import { PackumentNotFoundError } from "../../src/common-errors"; +import { PackumentNotFoundError } from "../../../src/common-errors"; describe("dependency graph", () => { const somePackage = DomainName.parse("com.some.package"); diff --git a/test/domain/domain-name.arb.ts b/test/unit/domain/domain-name.arb.ts similarity index 94% rename from test/domain/domain-name.arb.ts rename to test/unit/domain/domain-name.arb.ts index 154a4d54..51bb8cb5 100644 --- a/test/domain/domain-name.arb.ts +++ b/test/unit/domain/domain-name.arb.ts @@ -1,5 +1,5 @@ import fc from "fast-check"; -import { DomainName } from "../../src/domain/domain-name"; +import { DomainName } from "../../../src/domain/domain-name"; /** * Single char string [A-Z]. diff --git a/test/domain/domain-name.test.ts b/test/unit/domain/domain-name.test.ts similarity index 84% rename from test/domain/domain-name.test.ts rename to test/unit/domain/domain-name.test.ts index 75a44bb8..89c39265 100644 --- a/test/domain/domain-name.test.ts +++ b/test/unit/domain/domain-name.test.ts @@ -1,5 +1,5 @@ -import { isZod } from "../../src/utils/zod-utils"; -import { DomainName } from "../../src/domain/domain-name"; +import { isZod } from "../../../src/utils/zod-utils"; +import { DomainName } from "../../../src/domain/domain-name"; describe("domain-name", () => { describe("validation", () => { diff --git a/test/domain/editor-version.test.ts b/test/unit/domain/editor-version.test.ts similarity index 98% rename from test/domain/editor-version.test.ts rename to test/unit/domain/editor-version.test.ts index d278b989..c1bcbf47 100644 --- a/test/domain/editor-version.test.ts +++ b/test/unit/domain/editor-version.test.ts @@ -1,7 +1,7 @@ import { compareEditorVersion, tryParseEditorVersion, -} from "../../src/domain/editor-version"; +} from "../../../src/domain/editor-version"; import assert from "assert"; diff --git a/test/domain/npmrc.test.ts b/test/unit/domain/npmrc.test.ts similarity index 95% rename from test/domain/npmrc.test.ts rename to test/unit/domain/npmrc.test.ts index 219da751..97015f5e 100644 --- a/test/domain/npmrc.test.ts +++ b/test/unit/domain/npmrc.test.ts @@ -1,4 +1,4 @@ -import { emptyNpmrc, setToken } from "../../src/domain/npmrc"; +import { emptyNpmrc, setToken } from "../../../src/domain/npmrc"; import { exampleRegistryUrl } from "./data-registry"; diff --git a/test/domain/package-id.test.ts b/test/unit/domain/package-id.test.ts similarity index 93% rename from test/domain/package-id.test.ts rename to test/unit/domain/package-id.test.ts index b9012fca..eeb36753 100644 --- a/test/domain/package-id.test.ts +++ b/test/unit/domain/package-id.test.ts @@ -1,4 +1,4 @@ -import { isPackageId } from "../../src/domain/package-id"; +import { isPackageId } from "../../../src/domain/package-id"; import fc from "fast-check"; import { arbDomainName } from "./domain-name.arb"; diff --git a/test/domain/package-manifest.test.ts b/test/unit/domain/package-manifest.test.ts similarity index 91% rename from test/domain/package-manifest.test.ts rename to test/unit/domain/package-manifest.test.ts index b8876c54..a737d16b 100644 --- a/test/domain/package-manifest.test.ts +++ b/test/unit/domain/package-manifest.test.ts @@ -1,9 +1,9 @@ import { dependenciesOf, tryGetTargetEditorVersionFor, -} from "../../src/domain/package-manifest"; -import { makeEditorVersion } from "../../src/domain/editor-version"; -import { MalformedPackumentError } from "../../src/common-errors"; +} from "../../../src/domain/package-manifest"; +import { makeEditorVersion } from "../../../src/domain/editor-version"; +import { MalformedPackumentError } from "../../../src/common-errors"; describe("package manifest", () => { describe("get dependency list", () => { diff --git a/test/domain/package-reference.test.ts b/test/unit/domain/package-reference.test.ts similarity index 98% rename from test/domain/package-reference.test.ts rename to test/unit/domain/package-reference.test.ts index 1c050663..696132ba 100644 --- a/test/domain/package-reference.test.ts +++ b/test/unit/domain/package-reference.test.ts @@ -2,7 +2,7 @@ import { isPackageReference, makePackageReference, splitPackageReference, -} from "../../src/domain/package-reference"; +} from "../../../src/domain/package-reference"; import fc from "fast-check"; import { arbDomainName } from "./domain-name.arb"; diff --git a/test/domain/package-url.test.ts b/test/unit/domain/package-url.test.ts similarity index 92% rename from test/domain/package-url.test.ts rename to test/unit/domain/package-url.test.ts index 125322e5..31e9d033 100644 --- a/test/domain/package-url.test.ts +++ b/test/unit/domain/package-url.test.ts @@ -1,7 +1,7 @@ -import { PackageUrl } from "../../src/domain/package-url"; +import { PackageUrl } from "../../../src/domain/package-url"; import fc from "fast-check"; import { arbDomainName } from "./domain-name.arb"; -import { isZod } from "../../src/utils/zod-utils"; +import { isZod } from "../../../src/utils/zod-utils"; describe("package-url", () => { describe("validation", () => { diff --git a/test/domain/packument.test.ts b/test/unit/domain/packument.test.ts similarity index 79% rename from test/domain/packument.test.ts rename to test/unit/domain/packument.test.ts index e54881e3..ff1b6346 100644 --- a/test/domain/packument.test.ts +++ b/test/unit/domain/packument.test.ts @@ -1,13 +1,14 @@ +import { DomainName } from "../../../src/domain/domain-name"; import { NoVersionsError, + packumentHasVersion, tryGetLatestVersion, tryGetPackumentVersion, tryResolvePackumentVersion, VersionNotFoundError, -} from "../../src/domain/packument"; -import { SemanticVersion } from "../../src/domain/semantic-version"; +} from "../../../src/domain/packument"; +import { SemanticVersion } from "../../../src/domain/semantic-version"; import { buildPackument } from "./data-packument"; -import { DomainName } from "../../src/domain/domain-name"; describe("packument", () => { describe("get latest version", () => { @@ -122,4 +123,32 @@ describe("packument", () => { ); }); }); + + describe("has version", () => { + it("should have version if there is entry for it", () => { + const packument = buildPackument("com.some.package", (packument) => + packument.addVersion("1.0.0") + ); + + const hasVersion = packumentHasVersion( + packument, + SemanticVersion.parse("1.0.0") + ); + + expect(hasVersion).toBeTruthy(); + }); + + it("should not have version if there is no entry for it", () => { + const packument = buildPackument("com.some.package", (packument) => + packument.addVersion("1.0.0") + ); + + const hasVersion = packumentHasVersion( + packument, + SemanticVersion.parse("2.0.0") + ); + + expect(hasVersion).toBeFalsy(); + }); + }); }); diff --git a/test/domain/project-manifest.test.ts b/test/unit/domain/project-manifest.test.ts similarity index 95% rename from test/domain/project-manifest.test.ts rename to test/unit/domain/project-manifest.test.ts index 5c4359cc..ac4f22b3 100644 --- a/test/domain/project-manifest.test.ts +++ b/test/unit/domain/project-manifest.test.ts @@ -7,15 +7,15 @@ import { setDependency, setScopedRegistry, tryGetScopedRegistryByUrl, -} from "../../src/domain/project-manifest"; -import { DomainName } from "../../src/domain/domain-name"; -import { SemanticVersion } from "../../src/domain/semantic-version"; -import { addScope, makeScopedRegistry } from "../../src/domain/scoped-registry"; +} from "../../../src/domain/project-manifest"; +import { DomainName } from "../../../src/domain/domain-name"; +import { SemanticVersion } from "../../../src/domain/semantic-version"; +import { addScope, makeScopedRegistry } from "../../../src/domain/scoped-registry"; import fc from "fast-check"; import { arbDomainName } from "./domain-name.arb"; import { exampleRegistryUrl } from "./data-registry"; import { buildProjectManifest } from "./data-project-manifest"; -import { RegistryUrl } from "../../src/domain/registry-url"; +import { RegistryUrl } from "../../../src/domain/registry-url"; describe("project-manifest", () => { describe("set dependency", () => { diff --git a/test/domain/registry-url.test.ts b/test/unit/domain/registry-url.test.ts similarity index 85% rename from test/domain/registry-url.test.ts rename to test/unit/domain/registry-url.test.ts index 81251739..c6352e0b 100644 --- a/test/domain/registry-url.test.ts +++ b/test/unit/domain/registry-url.test.ts @@ -1,5 +1,5 @@ -import { coerceRegistryUrl, RegistryUrl } from "../../src/domain/registry-url"; -import { isZod } from "../../src/utils/zod-utils"; +import { coerceRegistryUrl, RegistryUrl } from "../../../src/domain/registry-url"; +import { isZod } from "../../../src/utils/zod-utils"; describe("registry-url", () => { describe("validation", () => { diff --git a/test/domain/scoped-registry.test.ts b/test/unit/domain/scoped-registry.test.ts similarity index 94% rename from test/domain/scoped-registry.test.ts rename to test/unit/domain/scoped-registry.test.ts index bf12323b..3e93155b 100644 --- a/test/domain/scoped-registry.test.ts +++ b/test/unit/domain/scoped-registry.test.ts @@ -4,12 +4,12 @@ import { makeEmptyScopedRegistryFor, makeScopedRegistry, removeScope, -} from "../../src/domain/scoped-registry"; -import { DomainName } from "../../src/domain/domain-name"; +} from "../../../src/domain/scoped-registry"; +import { DomainName } from "../../../src/domain/domain-name"; import fc from "fast-check"; import { arbDomainName } from "./domain-name.arb"; import { exampleRegistryUrl } from "./data-registry"; -import { unityRegistryUrl } from "../../src/domain/registry-url"; +import { unityRegistryUrl } from "../../../src/domain/registry-url"; describe("scoped-registry", () => { describe("construction", () => { diff --git a/test/domain/semantic-version.test.ts b/test/unit/domain/semantic-version.test.ts similarity index 75% rename from test/domain/semantic-version.test.ts rename to test/unit/domain/semantic-version.test.ts index 9d847e4e..0dffa320 100644 --- a/test/domain/semantic-version.test.ts +++ b/test/unit/domain/semantic-version.test.ts @@ -1,5 +1,5 @@ -import { SemanticVersion } from "../../src/domain/semantic-version"; -import { isZod } from "../../src/utils/zod-utils"; +import { SemanticVersion } from "../../../src/domain/semantic-version"; +import { isZod } from "../../../src/utils/zod-utils"; describe("semantic-version", () => { describe("validate", () => { diff --git a/test/unit/domain/upmconfig.test.ts b/test/unit/domain/upmconfig.test.ts new file mode 100644 index 00000000..d02b661f --- /dev/null +++ b/test/unit/domain/upmconfig.test.ts @@ -0,0 +1,57 @@ +import path from "path"; +import { + getUserUpmConfigPathFor, + NoSystemUserProfilePath, +} from "../../../src/domain/upm-config"; + +describe("upm config", () => { + describe("get user config path", () => { + const someHomeDirectory = path.resolve("/home/user/"); + const someAllUsersDirectory = path.resolve("/all-users/"); + + it("should be UPM_USER_CONFIG_FILE env if set", () => { + const expected = path.resolve("/home/user/.config/.upmconfig.toml"); + + const upmConfigPath = getUserUpmConfigPathFor( + { UPM_USER_CONFIG_FILE: expected }, + someHomeDirectory, + false + ); + + expect(upmConfigPath).toEqual(expected); + }); + + it("should be home path for regular users", () => { + const expected = path.join(someHomeDirectory, ".upmconfig.toml"); + + const upmConfigPath = getUserUpmConfigPathFor( + {}, + someHomeDirectory, + false + ); + + expect(upmConfigPath).toEqual(expected); + }); + + it("should be service account path for system user", () => { + const expected = path.join( + someAllUsersDirectory, + "/Unity/config/ServiceAccounts/.upmconfig.toml" + ); + + const upmConfigPath = getUserUpmConfigPathFor( + { ALLUSERSPROFILE: someAllUsersDirectory }, + someHomeDirectory, + true + ); + + expect(upmConfigPath).toEqual(expected); + }); + + it("should be fail for system user if required env is not set", () => { + expect(() => + getUserUpmConfigPathFor({}, someHomeDirectory, true) + ).toThrow(NoSystemUserProfilePath); + }); + }); +}); diff --git a/test/io/all-packuments-io.test.ts b/test/unit/io/all-packuments-io.test.ts similarity index 74% rename from test/io/all-packuments-io.test.ts rename to test/unit/io/all-packuments-io.test.ts index 3c0d9f98..8d284590 100644 --- a/test/io/all-packuments-io.test.ts +++ b/test/unit/io/all-packuments-io.test.ts @@ -1,9 +1,11 @@ import npmFetch from "npm-registry-fetch"; -import { HttpErrorBase } from "npm-registry-fetch/lib/errors"; -import { Registry } from "../../src/domain/registry"; -import { FetchAllRegistryPackuments } from "../../src/io/all-packuments-io"; -import { RegistryAuthenticationError } from "../../src/io/common-errors"; -import { noopLogger } from "../../src/logging"; +import { Registry } from "../../../src/domain/registry"; +import { getAllRegistryPackumentsUsing } from "../../../src/io/all-packuments-io"; +import { + HttpErrorLike, + RegistryAuthenticationError, +} from "../../../src/io/common-errors"; +import { noopLogger } from "../../../src/logging"; import { exampleRegistryUrl } from "../domain/data-registry"; jest.mock("npm-registry-fetch"); @@ -14,17 +16,17 @@ const exampleRegistry: Registry = { }; function makeDependencies() { - const fetchAllRegistryPackuments = FetchAllRegistryPackuments(noopLogger); + const fetchAllRegistryPackuments = getAllRegistryPackumentsUsing(noopLogger); return { fetchAllRegistryPackuments } as const; } describe("fetch all packuments", () => { it("should fail on non-auth error response", async () => { - const expected = { + const expected: HttpErrorLike = { message: "Idk, it failed", name: "FakeError", statusCode: 500, - } as HttpErrorBase; + }; jest.mocked(npmFetch.json).mockRejectedValue(expected); const { fetchAllRegistryPackuments } = makeDependencies(); @@ -34,11 +36,11 @@ describe("fetch all packuments", () => { }); it("should fail on auth error response", async () => { - const expected = { + const expected: HttpErrorLike = { message: "Idk, it failed", name: "FakeError", statusCode: 401, - } as HttpErrorBase; + }; jest.mocked(npmFetch.json).mockRejectedValue(expected); const { fetchAllRegistryPackuments } = makeDependencies(); diff --git a/test/io/builtin-packages.test.ts b/test/unit/io/builtin-packages.test.ts similarity index 82% rename from test/io/builtin-packages.test.ts rename to test/unit/io/builtin-packages.test.ts index 49e740bc..2038a02f 100644 --- a/test/io/builtin-packages.test.ts +++ b/test/unit/io/builtin-packages.test.ts @@ -1,18 +1,18 @@ -import * as specialPaths from "../../src/io/special-paths"; -import { OSNotSupportedError } from "../../src/io/special-paths"; import { Err, Ok } from "ts-results-es"; +import { makeEditorVersion } from "../../../src/domain/editor-version"; import { EditorNotInstalledError, makeFindBuiltInPackages, -} from "../../src/io/builtin-packages"; -import { makeEditorVersion } from "../../src/domain/editor-version"; -import { noopLogger } from "../../src/logging"; +} from "../../../src/io/builtin-packages"; +import { GetDirectoriesIn } from "../../../src/io/directory-io"; +import * as specialPaths from "../../../src/io/special-paths"; +import { OSNotSupportedError } from "../../../src/io/special-paths"; +import { noopLogger } from "../../../src/logging"; +import { mockFunctionOfType } from "../services/func.mock"; import { eaccesError, enoentError } from "./node-error.mock"; -import { mockService } from "../services/service.mock"; -import { GetDirectoriesIn } from "../../src/io/directory-io"; function makeDependencies() { - const getDirectoriesIn = mockService(); + const getDirectoriesIn = mockFunctionOfType(); const getBuiltInPackages = makeFindBuiltInPackages( getDirectoriesIn, diff --git a/test/unit/io/common-errors.test.ts b/test/unit/io/common-errors.test.ts new file mode 100644 index 00000000..2de6d4d8 --- /dev/null +++ b/test/unit/io/common-errors.test.ts @@ -0,0 +1,51 @@ +import { + makeRegistryInteractionError, + HttpErrorLike, + RegistryAuthenticationError, +} from "../../../src/io/common-errors"; +import { exampleRegistryUrl } from "../domain/data-registry"; + +describe("common error utilities", () => { + describe("make registry interaction errors", () => { + it("should be original error if not a http error", () => { + const error = new Error("Not http"); + + const actual = makeRegistryInteractionError( + error, + exampleRegistryUrl + ); + + expect(actual).toEqual(error); + }); + + it("should be original error if not a 401 error", () => { + const error: HttpErrorLike = { + name: "Some error", + message: "Http error", + statusCode: 404, + }; + + const actual = makeRegistryInteractionError( + error, + exampleRegistryUrl + ); + + expect(actual).toEqual(error); + }); + + it("should be auth error if 401 error", () => { + const error: HttpErrorLike = { + name: "Some error", + message: "Http error", + statusCode: 401, + }; + + const actual = makeRegistryInteractionError( + error, + exampleRegistryUrl + ); + + expect(actual).toBeInstanceOf(RegistryAuthenticationError); + }); + }); +}); diff --git a/test/io/directory-io.test.ts b/test/unit/io/directory-io.test.ts similarity index 95% rename from test/io/directory-io.test.ts rename to test/unit/io/directory-io.test.ts index b6e16ecb..5f4a6b94 100644 --- a/test/io/directory-io.test.ts +++ b/test/unit/io/directory-io.test.ts @@ -1,7 +1,7 @@ import { makeNodeError } from "./node-error.mock"; import fs from "fs/promises"; import { Dirent } from "node:fs"; -import { makeGetDirectoriesIn } from "../../src/io/directory-io"; +import { makeGetDirectoriesIn } from "../../../src/io/directory-io"; describe("directory io", () => { function makeDependencies() { diff --git a/test/io/node-error.mock.ts b/test/unit/io/node-error.mock.ts similarity index 100% rename from test/io/node-error.mock.ts rename to test/unit/io/node-error.mock.ts diff --git a/test/unit/io/npm-registry.test.ts b/test/unit/io/npm-registry.test.ts new file mode 100644 index 00000000..5553fbe2 --- /dev/null +++ b/test/unit/io/npm-registry.test.ts @@ -0,0 +1,73 @@ +import { userInfo } from "os"; +import { Registry } from "../../../src/domain/registry"; +import { makeNpmFetchOptions } from "../../../src/io/npm-registry"; +import { exampleRegistryUrl } from "../domain/data-registry"; +import { Options as FetchOptions } from "npm-registry-fetch"; +import { NpmAuth } from "another-npm-registry-client"; + +describe("npm registry interactions", () => { + describe("make fetch options from registry object", () => { + it("should use input registry url", () => { + const registry: Registry = { + url: exampleRegistryUrl, + auth: null, + }; + + const options = makeNpmFetchOptions(registry); + + expect(options.registry).toEqual(exampleRegistryUrl); + }); + + it("should have no auth data if input does not have one", () => { + const registry: Registry = { + url: exampleRegistryUrl, + auth: null, + }; + + const options = makeNpmFetchOptions(registry); + + expect(options.username).not.toBeDefined(); + expect(options.password).not.toBeDefined(); + expect(options.email).not.toBeDefined(); + expect(options.token).not.toBeDefined(); + expect(options.alwaysAuth).not.toBeDefined(); + }); + + it("should use token auth info", () => { + const auth: NpmAuth = { token: "Some token", alwaysAuth: true }; + const registry: Registry = { + url: exampleRegistryUrl, + auth, + }; + + const options = makeNpmFetchOptions(registry); + + expect(options.username).not.toBeDefined(); + expect(options.password).not.toBeDefined(); + expect(options.email).not.toBeDefined(); + expect(options.token).toEqual(auth.token); + expect(options.alwaysAuth).toEqual(auth.alwaysAuth); + }); + + it("should use basic auth info", () => { + const auth: NpmAuth = { + username: "user", + password: "pass", + email: "user@mail.com", + alwaysAuth: true, + }; + const registry: Registry = { + url: exampleRegistryUrl, + auth, + }; + + const options = makeNpmFetchOptions(registry); + + expect(options.username).toEqual(auth.username); + expect(options.password).toEqual(auth.password); + expect(options.email).toEqual(auth.email); + expect(options.token).not.toBeDefined(); + expect(options.alwaysAuth).toEqual(auth.alwaysAuth); + }); + }); +}); diff --git a/test/io/npm-search.test.ts b/test/unit/io/npm-search.test.ts similarity index 72% rename from test/io/npm-search.test.ts rename to test/unit/io/npm-search.test.ts index b8248a3d..e5a98ef6 100644 --- a/test/io/npm-search.test.ts +++ b/test/unit/io/npm-search.test.ts @@ -1,11 +1,12 @@ -import npmSearch from "libnpmsearch"; -import search from "libnpmsearch"; -import { HttpErrorBase } from "npm-registry-fetch/lib/errors"; -import { Registry } from "../../src/domain/registry"; +import { default as npmSearch, default as search } from "libnpmsearch"; +import { Registry } from "../../../src/domain/registry"; +import { + HttpErrorLike, + RegistryAuthenticationError, +} from "../../../src/io/common-errors"; +import { searchRegistryUsing } from "../../../src/io/npm-search"; +import { noopLogger } from "../../../src/logging"; import { exampleRegistryUrl } from "../domain/data-registry"; -import { noopLogger } from "../../src/logging"; -import { RegistryAuthenticationError } from "../../src/io/common-errors"; -import { NpmApiSearch } from "../../src/io/npm-search"; jest.mock("libnpmsearch"); @@ -16,16 +17,16 @@ const exampleRegistry: Registry = { describe("npm api search", () => { function makeDependencies() { - const npmApiSearch = NpmApiSearch(noopLogger); + const npmApiSearch = searchRegistryUsing(noopLogger); return { npmApiSearch } as const; } it("should fail for non-auth error response", async () => { - const expected = { + const expected: HttpErrorLike = { message: "Idk, it failed", name: "FakeError", statusCode: 500, - } as HttpErrorBase; + }; jest.mocked(npmSearch).mockRejectedValue(expected); const { npmApiSearch } = makeDependencies(); @@ -35,11 +36,11 @@ describe("npm api search", () => { }); it("should fail for auth error response", async () => { - const expected = { + const expected: HttpErrorLike = { message: "Idk, it failed", name: "FakeError", statusCode: 401, - } as HttpErrorBase; + }; jest.mocked(npmSearch).mockRejectedValue(expected); const { npmApiSearch } = makeDependencies(); diff --git a/test/io/npmrc-io.test.ts b/test/unit/io/npmrc-io.test.ts similarity index 71% rename from test/io/npmrc-io.test.ts rename to test/unit/io/npmrc-io.test.ts index dbe58279..e5a4919b 100644 --- a/test/io/npmrc-io.test.ts +++ b/test/unit/io/npmrc-io.test.ts @@ -4,26 +4,23 @@ import { FindNpmrcInHome, ReadNpmrcFile, WriteNpmrcPath as WriteNpmrcFile, -} from "../../src/io/npmrc-io"; -import { GetHomePath } from "../../src/io/special-paths"; -import { ReadTextFile, WriteTextFile } from "../../src/io/text-file-io"; -import { mockService } from "../services/service.mock"; +} from "../../../src/io/npmrc-io"; +import { ReadTextFile, WriteTextFile } from "../../../src/io/text-file-io"; +import { mockFunctionOfType } from "../services/func.mock"; describe("npmrc-io", () => { describe("find path in home", () => { - function makeDependencies() { - const getHomePath = mockService(); + const someHomePath = path.join(path.sep, "user", "dir"); - const findNpmrcInHome = FindNpmrcInHome(getHomePath); + function makeDependencies() { + const findNpmrcInHome = FindNpmrcInHome(someHomePath); - return { findNpmrcInHome, getHomePath } as const; + return { findNpmrcInHome } as const; } it("should be [Home]/.npmrc", () => { - const { findNpmrcInHome, getHomePath } = makeDependencies(); - const home = path.join(path.sep, "user", "dir"); - const expected = path.join(home, ".npmrc"); - getHomePath.mockReturnValue(home); + const { findNpmrcInHome } = makeDependencies(); + const expected = path.join(someHomePath, ".npmrc"); const actual = findNpmrcInHome(); @@ -33,7 +30,7 @@ describe("npmrc-io", () => { describe("read file", () => { function makeDependencies() { - const readText = mockService(); + const readText = mockFunctionOfType(); const readNpmrcFile = ReadNpmrcFile(readText); return { readNpmrcFile, readText } as const; @@ -62,7 +59,7 @@ describe("npmrc-io", () => { describe("write file", () => { function makeDependencies() { - const writeFile = mockService(); + const writeFile = mockFunctionOfType(); writeFile.mockResolvedValue(undefined); const writeNpmrcFile = WriteNpmrcFile(writeFile); diff --git a/test/io/packument-io.test.ts b/test/unit/io/packument-io.test.ts similarity index 82% rename from test/io/packument-io.test.ts rename to test/unit/io/packument-io.test.ts index 804d1300..e6656caa 100644 --- a/test/io/packument-io.test.ts +++ b/test/unit/io/packument-io.test.ts @@ -1,12 +1,11 @@ -import { buildPackument } from "../domain/data-packument"; -import { HttpErrorBase } from "npm-registry-fetch/lib/errors"; -import { DomainName } from "../../src/domain/domain-name"; import RegClient from "another-npm-registry-client"; -import { Registry } from "../../src/domain/registry"; +import { DomainName } from "../../../src/domain/domain-name"; +import { Registry } from "../../../src/domain/registry"; +import { getRegistryPackumentUsing } from "../../../src/io/packument-io"; +import { noopLogger } from "../../../src/logging"; +import { buildPackument } from "../domain/data-packument"; import { exampleRegistryUrl } from "../domain/data-registry"; import { mockRegClientGetResult } from "../services/registry-client.mock"; -import { FetchRegistryPackument } from "../../src/io/packument-io"; -import { noopLogger } from "../../src/logging"; describe("packument io", () => { describe("fetch", () => { @@ -22,7 +21,7 @@ describe("packument io", () => { get: jest.fn(), }; - const fetchRegistryPackument = FetchRegistryPackument( + const fetchRegistryPackument = getRegistryPackumentUsing( regClient, noopLogger ); @@ -47,7 +46,7 @@ describe("packument io", () => { message: "not found", name: "FakeError", statusCode: 404, - } as HttpErrorBase, + }, null ); @@ -64,7 +63,7 @@ describe("packument io", () => { message: "Unauthorized", name: "FakeError", statusCode: 401, - } as HttpErrorBase, + }, null ); diff --git a/test/unit/io/project-manifest-io.test.ts b/test/unit/io/project-manifest-io.test.ts new file mode 100644 index 00000000..f2ec705e --- /dev/null +++ b/test/unit/io/project-manifest-io.test.ts @@ -0,0 +1,118 @@ +import path from "path"; +import { UnityProjectManifest } from "../../../src/domain/project-manifest"; +import { + ManifestMalformedError, + ManifestMissingError, + manifestPathFor, + parseProjectManifest, + ReadProjectManifestFile, + serializeProjectManifest, +} from "../../../src/io/project-manifest-io"; +import { ReadTextFile } from "../../../src/io/text-file-io"; +import { noopLogger } from "../../../src/logging"; +import { exampleRegistryUrl } from "../domain/data-registry"; +import { mockFunctionOfType } from "../services/func.mock"; + +const exampleProjectPath = "/some/path"; +describe("project-manifest io", () => { + describe("parse", () => { + it("should parse valid manifest", () => { + const content = `{ "dependencies": { "com.package.a": "1.0.0"} }`; + + const parsed = parseProjectManifest(content); + + expect(parsed).toEqual({ + dependencies: { + "com.package.a": "1.0.0", + }, + }); + }); + + it("should fail for bad json", () => { + const content = "not : valid // json"; + + expect(() => parseProjectManifest(content)).toThrow(Error); + }); + + it("should fail incorrect json shape", () => { + // Valid json but not what we want + const content = `123`; + + expect(() => parseProjectManifest(content)).toThrow(Error); + }); + }); + + describe("path", () => { + it("should determine correct manifest path", () => { + const manifestPath = manifestPathFor("test-openupm-cli"); + const expected = path.join( + "test-openupm-cli", + "Packages", + "manifest.json" + ); + expect(manifestPath).toEqual(expected); + }); + }); + + describe("read file", () => { + function makeDependencies() { + const readFile = mockFunctionOfType(); + + const readProjectManifestFile = ReadProjectManifestFile( + readFile, + noopLogger + ); + return { readProjectManifestFile, readFile } as const; + } + + it("should fail if file is missing", async () => { + const { readProjectManifestFile, readFile } = makeDependencies(); + readFile.mockResolvedValue(null); + + await expect( + readProjectManifestFile(exampleProjectPath) + ).rejects.toBeInstanceOf(ManifestMissingError); + }); + + it("should fail if manifest could not be parsed", async () => { + const { readProjectManifestFile, readFile } = makeDependencies(); + readFile.mockResolvedValue("not {} valid : json"); + + await expect( + readProjectManifestFile(exampleProjectPath) + ).rejects.toBeInstanceOf(ManifestMalformedError); + }); + + it("should load valid manifest", async () => { + const { readProjectManifestFile, readFile } = makeDependencies(); + readFile.mockResolvedValue( + `{ "dependencies": { "com.package.a": "1.0.0"} }` + ); + + const actual = await readProjectManifestFile(exampleProjectPath); + + expect(actual).toEqual({ + dependencies: { + "com.package.a": "1.0.0", + }, + }); + }); + }); + + describe("serialize manifest", () => { + it("should prune empty scoped registries", () => { + const manifest: UnityProjectManifest = { + dependencies: {}, + scopedRegistries: [ + // Scoped registry with no scopes + { name: "some registry", url: exampleRegistryUrl, scopes: [] }, + ], + }; + + const json = serializeProjectManifest(manifest); + + // The registry should not be in the output json + expect(json).not.toContain("some registry"); + }); + }); +}); diff --git a/test/io/project-version-io.mock.ts b/test/unit/io/project-version-io.mock.ts similarity index 84% rename from test/io/project-version-io.mock.ts rename to test/unit/io/project-version-io.mock.ts index 6f6054da..6cf28fe4 100644 --- a/test/io/project-version-io.mock.ts +++ b/test/unit/io/project-version-io.mock.ts @@ -1,8 +1,8 @@ import { ReleaseVersion, stringifyEditorVersion, -} from "../../src/domain/editor-version"; -import { GetProjectVersion } from "../../src/io/project-version-io"; +} from "../../../src/domain/editor-version"; +import { GetProjectVersion } from "../../../src/io/project-version-io"; /** * Mocks return values for calls to a {@link GetProjectVersion} function. diff --git a/test/io/project-version-io.test.ts b/test/unit/io/project-version-io.test.ts similarity index 86% rename from test/io/project-version-io.test.ts rename to test/unit/io/project-version-io.test.ts index 60094196..affdffe7 100644 --- a/test/io/project-version-io.test.ts +++ b/test/unit/io/project-version-io.test.ts @@ -2,15 +2,15 @@ import { ProjectVersionMalformedError, ProjectVersionMissingError, ReadProjectVersionFile, -} from "../../src/io/project-version-io"; -import { ReadTextFile } from "../../src/io/text-file-io"; -import { noopLogger } from "../../src/logging"; -import { mockService } from "../services/service.mock"; +} from "../../../src/io/project-version-io"; +import { ReadTextFile } from "../../../src/io/text-file-io"; +import { noopLogger } from "../../../src/logging"; +import { mockFunctionOfType } from "../services/func.mock"; describe("project-version-io", () => { describe("read file", () => { function makeDependencies() { - const readFile = mockService(); + const readFile = mockFunctionOfType(); const readProjectVersionFile = ReadProjectVersionFile( readFile, diff --git a/test/io/special-paths.test.ts b/test/unit/io/special-paths.test.ts similarity index 81% rename from test/io/special-paths.test.ts rename to test/unit/io/special-paths.test.ts index ee3149ad..f0c221ca 100644 --- a/test/io/special-paths.test.ts +++ b/test/unit/io/special-paths.test.ts @@ -1,46 +1,35 @@ import os from "os"; import path from "path"; -import { EditorVersionNotSupportedError } from "../../src/common-errors"; -import { makeEditorVersion } from "../../src/domain/editor-version"; +import { EditorVersionNotSupportedError } from "../../../src/common-errors"; +import { makeEditorVersion } from "../../../src/domain/editor-version"; import { getHomePathFromEnv, NoHomePathError, OSNotSupportedError, tryGetEditorInstallPath, VersionNotSupportedOnOsError, -} from "../../src/io/special-paths"; -import { tryGetEnv } from "../../src/utils/env-util"; - -jest.mock("../../src/utils/env-util"); +} from "../../../src/io/special-paths"; describe("special-paths", () => { describe("home from env", () => { it("should be USERPROFILE if defined", () => { const expected = path.join(path.sep, "user", "dir"); - jest - .mocked(tryGetEnv) - .mockImplementation((key) => (key === "USERPROFILE" ? expected : null)); - const actual = getHomePathFromEnv(); + const actual = getHomePathFromEnv({ USERPROFILE: expected }); expect(actual).toEqual(expected); }); it("should be HOME if USERPROFILE is not defined", () => { const expected = path.join(path.sep, "user", "dir"); - jest - .mocked(tryGetEnv) - .mockImplementation((key) => (key === "HOME" ? expected : null)); - const actual = getHomePathFromEnv(); + const actual = getHomePathFromEnv({ HOME: expected }); expect(actual).toEqual(expected); }); it("should fail if HOME and USERPROFILE are not defined", () => { - jest.mocked(tryGetEnv).mockReturnValue(null); - - expect(() => getHomePathFromEnv()).toThrow(NoHomePathError); + expect(() => getHomePathFromEnv({})).toThrow(NoHomePathError); }); }); diff --git a/test/io/text-file-io.test.ts b/test/unit/io/text-file-io.test.ts similarity index 97% rename from test/io/text-file-io.test.ts rename to test/unit/io/text-file-io.test.ts index 0a29fb7a..87654d6c 100644 --- a/test/io/text-file-io.test.ts +++ b/test/unit/io/text-file-io.test.ts @@ -1,6 +1,6 @@ import fse from "fs-extra"; import fs from "fs/promises"; -import { readTextFile, writeTextFile } from "../../src/io/text-file-io"; +import { readTextFile, writeTextFile } from "../../../src/io/text-file-io"; import { makeNodeError } from "./node-error.mock"; describe("text file io", () => { diff --git a/test/io/upm-config-io.test.ts b/test/unit/io/upm-config-io.test.ts similarity index 62% rename from test/io/upm-config-io.test.ts rename to test/unit/io/upm-config-io.test.ts index 1a30a301..e4caa86d 100644 --- a/test/io/upm-config-io.test.ts +++ b/test/unit/io/upm-config-io.test.ts @@ -1,65 +1,19 @@ import { EOL } from "node:os"; -import path from "path"; -import { GetHomePath } from "../../src/io/special-paths"; -import { ReadTextFile } from "../../src/io/text-file-io"; -import { - ReadUpmConfigFile, - ResolveDefaultUpmConfigPath, -} from "../../src/io/upm-config-io"; +import { ReadTextFile } from "../../../src/io/text-file-io"; +import { ReadUpmConfigFile } from "../../../src/io/upm-config-io"; import { exampleRegistryUrl } from "../domain/data-registry"; -import { mockService } from "../services/service.mock"; -import { tryGetEnv } from "../../src/utils/env-util"; +import { mockFunctionOfType } from "../services/func.mock"; -jest.mock("../../src/utils/env-util"); +jest.mock("../../../src/utils/env-util"); describe("upm-config-io", () => { - describe("resolve default path", () => { - const homeConfigPath = path.resolve("/some/home/dir/.upmconfig.toml"); - const customConfigPath = path.resolve("/some/other/dir/.upmconfig.toml"); - - function makeDependencies() { - const getHomePath = mockService(); - getHomePath.mockReturnValue(path.dirname(homeConfigPath)); - - jest.mocked(tryGetEnv).mockReturnValue(null); - - const resolveDefaultUpmConfigPath = - ResolveDefaultUpmConfigPath(getHomePath); - - return { resolveDefaultUpmConfigPath, getHomePath } as const; - } - - it("should be UPM_USER_CONFIG_FILE if set", async () => { - const { resolveDefaultUpmConfigPath } = makeDependencies(); - jest - .mocked(tryGetEnv) - .mockImplementation((key) => - key === "UPM_USER_CONFIG_FILE" ? customConfigPath : null - ); - - const actual = await resolveDefaultUpmConfigPath(false); - - expect(actual).toEqual(customConfigPath); - }); - - describe("no system-user", () => { - it("should be in home path", async () => { - const { resolveDefaultUpmConfigPath } = makeDependencies(); - - const actual = await resolveDefaultUpmConfigPath(false); - - expect(actual).toEqual(homeConfigPath); - }); - }); - }); - describe("read file", () => { const someConfigPath = "/home/user/.upmconfig.toml"; const someEmail = "user@mail.com"; const someToken = "isehusehgusheguszg8gshg"; function makeDependencies() { - const readFile = mockService(); + const readFile = mockFunctionOfType(); readFile.mockResolvedValue(""); const readUpmConfigFile = ReadUpmConfigFile(readFile); diff --git a/test/services/built-in-package-check.test.ts b/test/unit/services/built-in-package-check.test.ts similarity index 61% rename from test/services/built-in-package-check.test.ts rename to test/unit/services/built-in-package-check.test.ts index 36298c15..a62d0566 100644 --- a/test/services/built-in-package-check.test.ts +++ b/test/unit/services/built-in-package-check.test.ts @@ -1,35 +1,35 @@ -import { DomainName } from "../../src/domain/domain-name"; -import { UnityPackument } from "../../src/domain/packument"; -import { SemanticVersion } from "../../src/domain/semantic-version"; -import { GetRegistryPackument } from "../../src/io/packument-io"; -import { CheckIsNonRegistryUnityPackage } from "../../src/services/built-in-package-check"; -import { CheckIsUnityPackage } from "../../src/services/unity-package-check"; -import { mockService } from "./service.mock"; +import { DomainName } from "../../../src/domain/domain-name"; +import { UnityPackument } from "../../../src/domain/packument"; +import { SemanticVersion } from "../../../src/domain/semantic-version"; +import { CheckUrlExists } from "../../../src/io/check-url"; +import { GetRegistryPackument } from "../../../src/io/packument-io"; +import { CheckIsNonRegistryUnityPackage } from "../../../src/services/built-in-package-check"; +import { mockFunctionOfType } from "./func.mock"; describe("is non-registry unity package", () => { const somePackage = DomainName.parse("com.some.package"); const someVersion = SemanticVersion.parse("1.0.0"); function makeDependencies() { - const checkIsUnityPackage = mockService(); + const checkUrlExists = mockFunctionOfType(); - const getRegistryPackument = mockService(); + const getRegistryPackument = mockFunctionOfType(); const checkIsNonRegistryUnityPackage = CheckIsNonRegistryUnityPackage( - checkIsUnityPackage, + checkUrlExists, getRegistryPackument ); return { checkIsNonRegistryUnityPackage, - checkIsUnityPackage, + checkUrlExists, getRegistryPackument, }; } it("should be false if package is not a Unity package", async () => { - const { checkIsNonRegistryUnityPackage, checkIsUnityPackage } = + const { checkIsNonRegistryUnityPackage, checkUrlExists } = makeDependencies(); - checkIsUnityPackage.mockResolvedValue(false); + checkUrlExists.mockResolvedValue(false); const actual = await checkIsNonRegistryUnityPackage( somePackage, @@ -42,10 +42,10 @@ describe("is non-registry unity package", () => { it("should be false if package is Unity package and exists on Unity registry", async () => { const { checkIsNonRegistryUnityPackage, - checkIsUnityPackage, + checkUrlExists, getRegistryPackument, } = makeDependencies(); - checkIsUnityPackage.mockResolvedValue(true); + checkUrlExists.mockResolvedValue(true); getRegistryPackument.mockResolvedValue({ versions: { [someVersion]: {} }, } as UnityPackument); @@ -61,10 +61,10 @@ describe("is non-registry unity package", () => { it("should be true if package is Unity package, but does not exist on Unity registry", async () => { const { checkIsNonRegistryUnityPackage, - checkIsUnityPackage, + checkUrlExists, getRegistryPackument, } = makeDependencies(); - checkIsUnityPackage.mockResolvedValue(true); + checkUrlExists.mockResolvedValue(true); getRegistryPackument.mockResolvedValue(null); const actual = await checkIsNonRegistryUnityPackage( diff --git a/test/services/dependency-resolving.test.ts b/test/unit/services/dependency-resolving.test.ts similarity index 86% rename from test/services/dependency-resolving.test.ts rename to test/unit/services/dependency-resolving.test.ts index 738b9462..e25b0378 100644 --- a/test/services/dependency-resolving.test.ts +++ b/test/unit/services/dependency-resolving.test.ts @@ -1,15 +1,15 @@ -import { mockService } from "./service.mock"; -import { GetRegistryPackument } from "../../src/io/packument-io"; -import { CheckIsBuiltInPackage } from "../../src/services/built-in-package-check"; +import { mockFunctionOfType } from "./func.mock"; +import { GetRegistryPackument } from "../../../src/io/packument-io"; +import { CheckIsBuiltInPackage } from "../../../src/services/built-in-package-check"; import { exampleRegistryUrl } from "../domain/data-registry"; -import { unityRegistryUrl } from "../../src/domain/registry-url"; -import { DomainName } from "../../src/domain/domain-name"; -import { SemanticVersion } from "../../src/domain/semantic-version"; -import { Registry } from "../../src/domain/registry"; -import { NodeType, tryGetGraphNode } from "../../src/domain/dependency-graph"; -import { PackumentNotFoundError } from "../../src/common-errors"; -import { VersionNotFoundError } from "../../src/domain/packument"; -import { ResolveDependenciesFromRegistries } from "../../src/services/dependency-resolving"; +import { unityRegistryUrl } from "../../../src/domain/registry-url"; +import { DomainName } from "../../../src/domain/domain-name"; +import { SemanticVersion } from "../../../src/domain/semantic-version"; +import { Registry } from "../../../src/domain/registry"; +import { NodeType, tryGetGraphNode } from "../../../src/domain/dependency-graph"; +import { PackumentNotFoundError } from "../../../src/common-errors"; +import { VersionNotFoundError } from "../../../src/domain/packument"; +import { ResolveDependenciesFromRegistries } from "../../../src/services/dependency-resolving"; describe("dependency resolving", () => { const sources: Registry[] = [ @@ -24,9 +24,9 @@ describe("dependency resolving", () => { const otherVersion = SemanticVersion.parse("2.0.0"); function makeDependencies() { - const getRegistryPackument = mockService(); + const getRegistryPackument = mockFunctionOfType(); - const checkIsBuiltInPackage = mockService(); + const checkIsBuiltInPackage = mockFunctionOfType(); checkIsBuiltInPackage.mockResolvedValue(false); const resolveDependencies = ResolveDependenciesFromRegistries( diff --git a/test/services/determine-editor-version.test.ts b/test/unit/services/determine-editor-version.test.ts similarity index 80% rename from test/services/determine-editor-version.test.ts rename to test/unit/services/determine-editor-version.test.ts index 122abbb8..ab1d26b7 100644 --- a/test/services/determine-editor-version.test.ts +++ b/test/unit/services/determine-editor-version.test.ts @@ -1,14 +1,14 @@ -import { mockService } from "./service.mock"; -import { GetProjectVersion } from "../../src/io/project-version-io"; -import { makeEditorVersion } from "../../src/domain/editor-version"; +import { mockFunctionOfType } from "./func.mock"; +import { GetProjectVersion } from "../../../src/io/project-version-io"; +import { makeEditorVersion } from "../../../src/domain/editor-version"; import { mockProjectVersion } from "../io/project-version-io.mock"; -import { DetermineEditorVersionFromFile } from "../../src/services/determine-editor-version"; +import { DetermineEditorVersionFromFile } from "../../../src/services/determine-editor-version"; describe("determine editor version from file", () => { const exampleProjectPath = "/home/my-project/"; function makeDependencies() { - const getProjectVersion = mockService(); + const getProjectVersion = mockFunctionOfType(); const determineEditorVersionFromFile = DetermineEditorVersionFromFile(getProjectVersion); diff --git a/test/unit/services/func.mock.ts b/test/unit/services/func.mock.ts new file mode 100644 index 00000000..35e8c858 --- /dev/null +++ b/test/unit/services/func.mock.ts @@ -0,0 +1,28 @@ +/** + * Creates a mock for a function of a specific type. + * This is just a typing-utility function. Under the hood this just calls + * {@link jest.fn}. + */ +export function mockFunctionOfType< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TFunc extends (...args: any[]) => any +>(): jest.MockedFunction { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return jest.fn(); +} + +/** + * Creates a mock for a function with a specific return type. + */ +export function mockFunctionWithReturnType() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return mockFunctionOfType<(...args: any[]) => T>(); +} + +/** + * Creates a mock for a function with a specific resolved type. + */ +export function mockFunctionWithResolvedType() { + return mockFunctionWithReturnType>(); +} diff --git a/test/services/get-auth-token.ts b/test/unit/services/get-auth-token.test.ts similarity index 88% rename from test/services/get-auth-token.ts rename to test/unit/services/get-auth-token.test.ts index abac4ccd..54f8895e 100644 --- a/test/services/get-auth-token.ts +++ b/test/unit/services/get-auth-token.test.ts @@ -1,11 +1,10 @@ import "assert"; import RegClient from "another-npm-registry-client"; -import { HttpErrorBase } from "npm-registry-fetch/lib/errors"; import { exampleRegistryUrl } from "../domain/data-registry"; import { mockRegClientAddUserResult } from "./registry-client.mock"; -import { RegistryAuthenticationError } from "../../src/io/common-errors"; -import { noopLogger } from "../../src/logging"; -import { AuthenticateWithNpmRegistry } from "../../src/services/get-auth-token"; +import { RegistryAuthenticationError } from "../../../src/io/common-errors"; +import { noopLogger } from "../../../src/logging"; +import { AuthenticateWithNpmRegistry } from "../../../src/services/get-auth-token"; describe("authenticate user with npm registry", () => { function makeDependencies() { @@ -74,7 +73,7 @@ describe("authenticate user with npm registry", () => { makeDependencies(); mockRegClientAddUserResult( registryClient, - {} as HttpErrorBase, + new Error(), { ok: false }, { statusMessage: "bad user", diff --git a/test/services/get-latest-version.test.ts b/test/unit/services/get-latest-version.test.ts similarity index 74% rename from test/services/get-latest-version.test.ts rename to test/unit/services/get-latest-version.test.ts index 01308aa6..79a5ce4f 100644 --- a/test/services/get-latest-version.test.ts +++ b/test/unit/services/get-latest-version.test.ts @@ -1,11 +1,11 @@ -import { DomainName } from "../../src/domain/domain-name"; -import { UnityPackument } from "../../src/domain/packument"; -import { Registry } from "../../src/domain/registry"; -import { SemanticVersion } from "../../src/domain/semantic-version"; -import { GetRegistryPackument } from "../../src/io/packument-io"; -import { GetLatestVersionFromRegistryPackument } from "../../src/services/get-latest-version"; +import { DomainName } from "../../../src/domain/domain-name"; +import { UnityPackument } from "../../../src/domain/packument"; +import { Registry } from "../../../src/domain/registry"; +import { SemanticVersion } from "../../../src/domain/semantic-version"; +import { GetRegistryPackument } from "../../../src/io/packument-io"; +import { GetLatestVersionFromRegistryPackument } from "../../../src/services/get-latest-version"; import { exampleRegistryUrl } from "../domain/data-registry"; -import { mockService } from "./service.mock"; +import { mockFunctionOfType } from "./func.mock"; describe("get latest version from registry packument", () => { const somePackage = DomainName.parse("com.some.package"); @@ -13,7 +13,7 @@ describe("get latest version from registry packument", () => { const exampleRegistry: Registry = { url: exampleRegistryUrl, auth: null }; function makeDependencies() { - const getRegistryPackument = mockService(); + const getRegistryPackument = mockFunctionOfType(); const getLatestVersionFromRegistryPackument = GetLatestVersionFromRegistryPackument(getRegistryPackument); diff --git a/test/unit/services/get-registry-auth.test.ts b/test/unit/services/get-registry-auth.test.ts new file mode 100644 index 00000000..ffd36b73 --- /dev/null +++ b/test/unit/services/get-registry-auth.test.ts @@ -0,0 +1,140 @@ +import { Base64 } from "../../../src/domain/base64"; +import { + openupmRegistryUrl, + unityRegistryUrl, +} from "../../../src/domain/registry-url"; +import { GetUpmConfigPath, LoadUpmConfig } from "../../../src/io/upm-config-io"; +import { noopLogger } from "../../../src/logging"; +import { LoadRegistryAuthFromUpmConfig } from "../../../src/services/get-registry-auth"; +import { exampleRegistryUrl } from "../domain/data-registry"; +import { mockFunctionOfType } from "./func.mock"; + +describe("get registry auth from upm config", () => { + const someEmail = "user@mail.com"; + const someToken = "isehusehgusheguszg8gshg"; + + function makeDependencies() { + const getUpmConfigPath = mockFunctionOfType(); + getUpmConfigPath.mockReturnValue("/home/user/.upmconfig.toml"); + + const loadUpmConfig = mockFunctionOfType(); + loadUpmConfig.mockResolvedValue({}); + + const getRegistryAuth = LoadRegistryAuthFromUpmConfig( + getUpmConfigPath, + loadUpmConfig, + noopLogger + ); + return { getRegistryAuth, loadUpmConfig } as const; + } + + it("should have no auth if no .upmconfig.toml file", async () => { + const { getRegistryAuth, loadUpmConfig } = makeDependencies(); + loadUpmConfig.mockResolvedValue(null); + + const registry = await getRegistryAuth(false, exampleRegistryUrl); + + expect(registry.auth).toBeNull(); + }); + + it("should have no auth if there is no entry for registry", async () => { + const { getRegistryAuth, loadUpmConfig } = makeDependencies(); + loadUpmConfig.mockResolvedValue({}); + + const registry = await getRegistryAuth(false, exampleRegistryUrl); + + expect(registry.auth).toBeNull(); + }); + + it("should get valid basic auth", async () => { + const { getRegistryAuth, loadUpmConfig } = makeDependencies(); + loadUpmConfig.mockResolvedValue({ + npmAuth: { + [exampleRegistryUrl]: { + _auth: "dXNlcjpwYXNz" as Base64, // user:pass + email: someEmail, + alwaysAuth: true, + }, + }, + }); + + const registry = await getRegistryAuth(false, exampleRegistryUrl); + + expect(registry.auth).toEqual({ + username: "user", + password: "pass", + email: someEmail, + alwaysAuth: true, + }); + }); + + it("should get valid token auth", async () => { + const { getRegistryAuth, loadUpmConfig } = makeDependencies(); + loadUpmConfig.mockResolvedValue({ + npmAuth: { [exampleRegistryUrl]: { token: someToken, alwaysAuth: true } }, + }); + + const registry = await getRegistryAuth(false, exampleRegistryUrl); + + expect(registry.auth).toEqual({ + token: someToken, + alwaysAuth: true, + }); + }); + + it("should ignore email when getting token auth", async () => { + const { getRegistryAuth, loadUpmConfig } = makeDependencies(); + loadUpmConfig.mockResolvedValue({ + npmAuth: { + [exampleRegistryUrl]: { + token: someToken, + email: someEmail, + }, + }, + }); + + const registry = await getRegistryAuth(false, exampleRegistryUrl); + + expect(registry.auth).toEqual({ + token: someToken, + }); + }); + + it("should get auth for url with trailing slash", async () => { + const { getRegistryAuth, loadUpmConfig } = makeDependencies(); + loadUpmConfig.mockResolvedValue({ + npmAuth: { [exampleRegistryUrl + "/"]: { token: someToken } }, + }); + + const registry = await getRegistryAuth(false, exampleRegistryUrl); + + expect(registry.auth).toEqual({ + token: someToken, + }); + }); + + it("should not load upmconfig for openupm registry url", async () => { + const { getRegistryAuth, loadUpmConfig } = makeDependencies(); + + await getRegistryAuth(false, openupmRegistryUrl); + + expect(loadUpmConfig).not.toHaveBeenCalled(); + }); + + it("should not load upmconfig for unity registry url", async () => { + const { getRegistryAuth, loadUpmConfig } = makeDependencies(); + + await getRegistryAuth(false, unityRegistryUrl); + + expect(loadUpmConfig).not.toHaveBeenCalled(); + }); + + it("should cache .upmconfig.toml content", async () => { + const { getRegistryAuth, loadUpmConfig } = makeDependencies(); + + await getRegistryAuth(false, exampleRegistryUrl); + await getRegistryAuth(false, exampleRegistryUrl); + + expect(loadUpmConfig).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/services/get-registry-packument-version.test.ts b/test/unit/services/get-registry-packument-version.test.ts similarity index 77% rename from test/services/get-registry-packument-version.test.ts rename to test/unit/services/get-registry-packument-version.test.ts index bac53e2b..da9ff4af 100644 --- a/test/services/get-registry-packument-version.test.ts +++ b/test/unit/services/get-registry-packument-version.test.ts @@ -1,12 +1,12 @@ -import { PackumentNotFoundError } from "../../src/common-errors"; -import { DomainName } from "../../src/domain/domain-name"; -import { Registry } from "../../src/domain/registry"; -import { SemanticVersion } from "../../src/domain/semantic-version"; -import { GetRegistryPackument } from "../../src/io/packument-io"; -import { FetchRegistryPackumentVersion } from "../../src/services/get-registry-packument-version"; +import { PackumentNotFoundError } from "../../../src/common-errors"; +import { DomainName } from "../../../src/domain/domain-name"; +import { Registry } from "../../../src/domain/registry"; +import { SemanticVersion } from "../../../src/domain/semantic-version"; +import { GetRegistryPackument } from "../../../src/io/packument-io"; +import { FetchRegistryPackumentVersion } from "../../../src/services/get-registry-packument-version"; import { buildPackument } from "../domain/data-packument"; import { exampleRegistryUrl } from "../domain/data-registry"; -import { mockService } from "./service.mock"; +import { mockFunctionOfType } from "./func.mock"; describe("fetch registrypackument version", () => { const somePackage = DomainName.parse("com.some.package"); @@ -16,7 +16,7 @@ describe("fetch registrypackument version", () => { const someRegistry: Registry = { url: exampleRegistryUrl, auth: null }; function makeDependencies() { - const getRegistryPackument = mockService(); + const getRegistryPackument = mockFunctionOfType(); const fetchRegistryPackumentVersion = FetchRegistryPackumentVersion(getRegistryPackument); diff --git a/test/services/login.test.ts b/test/unit/services/login.test.ts similarity index 76% rename from test/services/login.test.ts rename to test/unit/services/login.test.ts index e34c7edf..c166eb4a 100644 --- a/test/services/login.test.ts +++ b/test/unit/services/login.test.ts @@ -1,10 +1,10 @@ -import { noopLogger } from "../../src/logging"; -import { GetAuthToken } from "../../src/services/get-auth-token"; -import { UpmConfigLogin } from "../../src/services/login"; -import { StoreNpmAuthToken } from "../../src/services/put-npm-auth-token"; -import { PutRegistryAuth } from "../../src/services/put-registry-auth"; +import { noopLogger } from "../../../src/logging"; +import { GetAuthToken } from "../../../src/services/get-auth-token"; +import { UpmConfigLogin } from "../../../src/services/login"; +import { StoreNpmAuthToken } from "../../../src/services/put-npm-auth-token"; +import { PutRegistryAuth } from "../../../src/services/put-registry-auth"; import { exampleRegistryUrl } from "../domain/data-registry"; -import { mockService } from "./service.mock"; +import { mockFunctionOfType } from "./func.mock"; const exampleUser = "user"; const examplePassword = "pass"; @@ -15,13 +15,13 @@ const exampleToken = "some token"; describe("login", () => { function makeDependencies() { - const putRegistryAuth = mockService(); + const putRegistryAuth = mockFunctionOfType(); putRegistryAuth.mockResolvedValue(undefined); - const npmLogin = mockService(); + const npmLogin = mockFunctionOfType(); npmLogin.mockResolvedValue(exampleToken); - const putNpmAuthToken = mockService(); + const putNpmAuthToken = mockFunctionOfType(); putNpmAuthToken.mockResolvedValue(exampleNpmrcPath); const login = UpmConfigLogin( diff --git a/test/services/parse-env.test.ts b/test/unit/services/parse-env.test.ts similarity index 55% rename from test/services/parse-env.test.ts rename to test/unit/services/parse-env.test.ts index 756a6171..65f44dfc 100644 --- a/test/services/parse-env.test.ts +++ b/test/unit/services/parse-env.test.ts @@ -1,53 +1,16 @@ -import { NpmAuth } from "another-npm-registry-client"; import path from "path"; -import { emptyUpmConfig, UpmConfig } from "../../src/domain/upm-config"; -import { GetCwd } from "../../src/io/special-paths"; -import { GetUpmConfigPath } from "../../src/io/upm-config-io"; -import { noopLogger } from "../../src/logging"; -import { GetRegistryAuth } from "../../src/services/get-registry-auth"; -import { makeParseEnv } from "../../src/services/parse-env"; +import { openupmRegistryUrl } from "../../../src/domain/registry-url"; +import { makeParseEnv } from "../../../src/services/parse-env"; import { makeMockLogger } from "../cli/log.mock"; import { exampleRegistryUrl } from "../domain/data-registry"; -import { mockService } from "./service.mock"; const testRootPath = "/users/some-user/projects/MyUnityProject"; -const testNpmAuth: NpmAuth = { - token: "ThisIsNotAValidToken", - alwaysAuth: false, -}; - -const testUpmConfig: UpmConfig = { - [exampleRegistryUrl]: testNpmAuth, -}; - function makeDependencies() { const log = makeMockLogger(); - const getUpmConfigPath = mockService(); - // The root directory does not contain an upm-config - getUpmConfigPath.mockResolvedValue(testRootPath); - - const loadRegistryAuth = mockService(); - loadRegistryAuth.mockResolvedValue(emptyUpmConfig); - - // process.cwd is in the root directory. - const getCwd = mockService(); - getCwd.mockReturnValue(testRootPath); - - const parseEnv = makeParseEnv( - log, - getUpmConfigPath, - loadRegistryAuth, - getCwd, - noopLogger - ); - return { - parseEnv, - log, - getUpmConfigPath, - loadRegistryAuth, - } as const; + const parseEnv = makeParseEnv(log, testRootPath); + return { parseEnv, log } as const; } describe("env", () => { @@ -178,86 +141,22 @@ describe("env", () => { }); describe("registry", () => { - it("should be global openupm by default", async () => { + it("should be openupm by default", async () => { const { parseEnv } = makeDependencies(); const env = await parseEnv({}); - expect(env.registry.url).toEqual("https://package.openupm.com"); + expect(env.primaryRegistryUrl).toEqual(openupmRegistryUrl); }); - it("should be custom registry if overridden", async () => { + it("should be custom registry url if overridden", async () => { const { parseEnv } = makeDependencies(); const env = await parseEnv({ registry: exampleRegistryUrl, }); - expect(env.registry.url).toEqual(exampleRegistryUrl); - }); - - it("should have no auth if no upm-config was found", async () => { - const { parseEnv } = makeDependencies(); - - const env = await parseEnv({ - registry: exampleRegistryUrl, - }); - - expect(env.registry.auth).toEqual(null); - }); - - it("should have no auth if upm-config had no entry for the url", async () => { - const { parseEnv, loadRegistryAuth } = makeDependencies(); - loadRegistryAuth.mockResolvedValue({}); - - const env = await parseEnv({ - registry: exampleRegistryUrl, - }); - - expect(env.registry.auth).toEqual(null); - }); - - it("should notify if upm-config did not have auth", async () => { - const { parseEnv, log, loadRegistryAuth } = makeDependencies(); - loadRegistryAuth.mockResolvedValue({}); - - await parseEnv({ - registry: exampleRegistryUrl, - }); - - expect(log.verbose).toHaveBeenCalledWith( - "", - expect.stringContaining("did not contain an entry") - ); - }); - - it("should have auth if upm-config had entry for the url", async () => { - const { parseEnv, loadRegistryAuth } = makeDependencies(); - loadRegistryAuth.mockResolvedValue(testUpmConfig); - - const env = await parseEnv({ - registry: exampleRegistryUrl, - }); - - expect(env.registry.auth).toEqual(testNpmAuth); - }); - }); - - describe("upstream registry", () => { - it("should be global unity by default", async () => { - const { parseEnv } = makeDependencies(); - - const env = await parseEnv({}); - - expect(env.upstreamRegistry.url).toEqual("https://packages.unity.com"); - }); - - it("should have no auth", async () => { - const { parseEnv } = makeDependencies(); - - const env = await parseEnv({}); - - expect(env.upstreamRegistry.auth).toEqual(null); + expect(env.primaryRegistryUrl).toEqual(exampleRegistryUrl); }); }); diff --git a/test/services/put-npm-auth-token.test.ts b/test/unit/services/put-npm-auth-token.test.ts similarity index 80% rename from test/services/put-npm-auth-token.test.ts rename to test/unit/services/put-npm-auth-token.test.ts index 4f9d4bd2..dc5d40ea 100644 --- a/test/services/put-npm-auth-token.test.ts +++ b/test/unit/services/put-npm-auth-token.test.ts @@ -1,20 +1,20 @@ -import { emptyNpmrc, setToken } from "../../src/domain/npmrc"; -import { FindNpmrcPath, LoadNpmrc, SaveNpmrc } from "../../src/io/npmrc-io"; -import { StoreNpmAuthTokenInNpmrc as PutNpmAuthTokenInNpmrc } from "../../src/services/put-npm-auth-token"; +import { emptyNpmrc, setToken } from "../../../src/domain/npmrc"; +import { FindNpmrcPath, LoadNpmrc, SaveNpmrc } from "../../../src/io/npmrc-io"; +import { StoreNpmAuthTokenInNpmrc as PutNpmAuthTokenInNpmrc } from "../../../src/services/put-npm-auth-token"; import { exampleRegistryUrl } from "../domain/data-registry"; -import { mockService } from "./service.mock"; +import { mockFunctionOfType } from "./func.mock"; const exampleNpmrcPath = "/users/someuser/.npmrc"; describe("put npm auth token in npmrc", () => { function makeDependencies() { - const findPath = mockService(); + const findPath = mockFunctionOfType(); findPath.mockReturnValue(exampleNpmrcPath); - const loadNpmrc = mockService(); + const loadNpmrc = mockFunctionOfType(); loadNpmrc.mockResolvedValue(null); - const saveNpmrc = mockService(); + const saveNpmrc = mockFunctionOfType(); saveNpmrc.mockResolvedValue(undefined); const putNpmAuthTokenInNpmrc = PutNpmAuthTokenInNpmrc( diff --git a/test/services/put-registry-auth.test.ts b/test/unit/services/put-registry-auth.test.ts similarity index 92% rename from test/services/put-registry-auth.test.ts rename to test/unit/services/put-registry-auth.test.ts index 458d4dd0..ca1de26d 100644 --- a/test/services/put-registry-auth.test.ts +++ b/test/unit/services/put-registry-auth.test.ts @@ -1,8 +1,8 @@ -import { mockService } from "./service.mock"; -import { LoadUpmConfig, SaveUpmConfig } from "../../src/io/upm-config-io"; +import { mockFunctionOfType } from "./func.mock"; +import { LoadUpmConfig, SaveUpmConfig } from "../../../src/io/upm-config-io"; import { exampleRegistryUrl } from "../domain/data-registry"; -import { Base64 } from "../../src/domain/base64"; -import { PutRegistryAuthIntoUpmConfig } from "../../src/services/put-registry-auth"; +import { Base64 } from "../../../src/domain/base64"; +import { PutRegistryAuthIntoUpmConfig } from "../../../src/services/put-registry-auth"; describe("put registry auth into upm config", () => { const someConfigPath = "/home/user/.upmconfig.toml"; @@ -11,10 +11,10 @@ describe("put registry auth into upm config", () => { const otherToken = "8zseg974wge4g94whfheghf"; function makeDependencies() { - const loadUpmConfig = mockService(); + const loadUpmConfig = mockFunctionOfType(); loadUpmConfig.mockResolvedValue({}); - const saveUpmConfig = mockService(); + const saveUpmConfig = mockFunctionOfType(); saveUpmConfig.mockResolvedValue(); const putRegistryAuth = PutRegistryAuthIntoUpmConfig( diff --git a/test/services/registry-client.mock.ts b/test/unit/services/registry-client.mock.ts similarity index 86% rename from test/services/registry-client.mock.ts rename to test/unit/services/registry-client.mock.ts index 1912ce22..d781126c 100644 --- a/test/services/registry-client.mock.ts +++ b/test/unit/services/registry-client.mock.ts @@ -1,7 +1,7 @@ import RegClient, { AddUserResponse } from "another-npm-registry-client"; -import { UnityPackument } from "../../src/domain/packument"; -import { HttpErrorBase } from "npm-registry-fetch/lib/errors"; import { Response } from "request"; +import { UnityPackument } from "../../../src/domain/packument"; +import { HttpErrorLike } from "../../../src/io/common-errors"; /** * Mocks the result of getting a package using a {@link RegClient.Instance}. @@ -11,7 +11,7 @@ import { Response } from "request"; */ export function mockRegClientGetResult( regClient: jest.Mocked, - error: HttpErrorBase | null, + error: Error | HttpErrorLike | null, packument: UnityPackument | null ) { regClient.get.mockImplementation((_1, _2, cb) => { @@ -28,7 +28,7 @@ export function mockRegClientGetResult( */ export function mockRegClientAddUserResult( registryClient: jest.Mocked, - error: HttpErrorBase | null, + error: Error | null, responseData: AddUserResponse | null, response: Pick | null ) { diff --git a/test/services/remote-packuments.mock.ts b/test/unit/services/remote-packuments.mock.ts similarity index 78% rename from test/services/remote-packuments.mock.ts rename to test/unit/services/remote-packuments.mock.ts index 47cb49c3..f2ed7f99 100644 --- a/test/services/remote-packuments.mock.ts +++ b/test/unit/services/remote-packuments.mock.ts @@ -1,11 +1,11 @@ -import { PackumentNotFoundError } from "../../src/common-errors"; +import { PackumentNotFoundError } from "../../../src/common-errors"; import { tryResolvePackumentVersion, UnityPackument, -} from "../../src/domain/packument"; -import { RegistryUrl } from "../../src/domain/registry-url"; -import { GetRegistryPackumentVersion } from "../../src/services/get-registry-packument-version"; -import { AsyncErr } from "../../src/utils/result-utils"; +} from "../../../src/domain/packument"; +import { RegistryUrl } from "../../../src/domain/registry-url"; +import { GetRegistryPackumentVersion } from "../../../src/services/get-registry-packument-version"; +import { AsyncErr } from "../../../src/utils/result-utils"; type MockEntry = [RegistryUrl, UnityPackument]; diff --git a/test/services/remove-packages.test.ts b/test/unit/services/remove-packages.test.ts similarity index 87% rename from test/services/remove-packages.test.ts rename to test/unit/services/remove-packages.test.ts index fb7de970..cb80bc5d 100644 --- a/test/services/remove-packages.test.ts +++ b/test/unit/services/remove-packages.test.ts @@ -1,16 +1,16 @@ import path from "path"; -import { PackumentNotFoundError } from "../../src/common-errors"; -import { DomainName } from "../../src/domain/domain-name"; +import { PackumentNotFoundError } from "../../../src/common-errors"; +import { DomainName } from "../../../src/domain/domain-name"; import { LoadProjectManifest, SaveProjectManifest, -} from "../../src/io/project-manifest-io"; +} from "../../../src/io/project-manifest-io"; import { RemovedPackage, RemovePackagesFromManifest, -} from "../../src/services/remove-packages"; +} from "../../../src/services/remove-packages"; import { buildProjectManifest } from "../domain/data-project-manifest"; -import { mockService } from "./service.mock"; +import { mockFunctionOfType } from "./func.mock"; describe("remove packages from manifest", () => { const someProjectPath = path.resolve("/home/projects/MyUnityProject"); @@ -22,10 +22,10 @@ describe("remove packages from manifest", () => { ); function makeDependencies() { - const loadProjectManifest = mockService(); + const loadProjectManifest = mockFunctionOfType(); loadProjectManifest.mockResolvedValue(defaultManifest); - const writeProjectManifest = mockService(); + const writeProjectManifest = mockFunctionOfType(); writeProjectManifest.mockResolvedValue(undefined); const removePackagesFromManifestu = RemovePackagesFromManifest( diff --git a/test/services/search-packages.test.ts b/test/unit/services/search-packages.test.ts similarity index 80% rename from test/services/search-packages.test.ts rename to test/unit/services/search-packages.test.ts index ca982454..b8ac1da7 100644 --- a/test/services/search-packages.test.ts +++ b/test/unit/services/search-packages.test.ts @@ -1,15 +1,15 @@ -import { DomainName } from "../../src/domain/domain-name"; -import { Registry } from "../../src/domain/registry"; -import { SemanticVersion } from "../../src/domain/semantic-version"; +import { DomainName } from "../../../src/domain/domain-name"; +import { Registry } from "../../../src/domain/registry"; +import { SemanticVersion } from "../../../src/domain/semantic-version"; import { AllPackuments, GetAllRegistryPackuments, -} from "../../src/io/all-packuments-io"; -import { SearchedPackument, SearchRegistry } from "../../src/io/npm-search"; -import { noopLogger } from "../../src/logging"; -import { ApiAndFallbackPackageSearch } from "../../src/services/search-packages"; +} from "../../../src/io/all-packuments-io"; +import { SearchedPackument, SearchRegistry } from "../../../src/io/npm-search"; +import { noopLogger } from "../../../src/logging"; +import { ApiAndFallbackPackageSearch } from "../../../src/services/search-packages"; import { exampleRegistryUrl } from "../domain/data-registry"; -import { mockService } from "./service.mock"; +import { mockFunctionOfType } from "./func.mock"; describe("search packages", () => { const exampleRegistry: Registry = { @@ -33,10 +33,10 @@ describe("search packages", () => { } as AllPackuments; function makeDependencies() { - const searchRegistry = mockService(); + const searchRegistry = mockFunctionOfType(); searchRegistry.mockResolvedValue([exampleSearchResult]); - const fetchAllPackument = mockService(); + const fetchAllPackument = mockFunctionOfType(); fetchAllPackument.mockResolvedValue(exampleAllPackumentsResult); const searchPackages = ApiAndFallbackPackageSearch( diff --git a/test/utils/sources.test.ts b/test/unit/utils/sources.test.ts similarity index 92% rename from test/utils/sources.test.ts rename to test/unit/utils/sources.test.ts index 3a02f93d..c3426ae1 100644 --- a/test/utils/sources.test.ts +++ b/test/unit/utils/sources.test.ts @@ -1,7 +1,7 @@ -import { queryAllRegistriesLazy } from "../../src/utils/sources"; -import { Registry } from "../../src/domain/registry"; +import { queryAllRegistriesLazy } from "../../../src/utils/sources"; +import { Registry } from "../../../src/domain/registry"; import { exampleRegistryUrl } from "../domain/data-registry"; -import { unityRegistryUrl } from "../../src/domain/registry-url"; +import { unityRegistryUrl } from "../../../src/domain/registry-url"; describe("sources", () => { describe("query registries lazy", () => { diff --git a/test/unit/utils/string-utils.test.ts b/test/unit/utils/string-utils.test.ts new file mode 100644 index 00000000..d80ad4f3 --- /dev/null +++ b/test/unit/utils/string-utils.test.ts @@ -0,0 +1,17 @@ +import { splitLines } from "../../../src/utils/string-utils"; + +describe("string utils", () => { + describe("split into lines", () => { + it("should work for any linebreak string", () => { + const content = "A\nB\r\nC\rD"; + const lines = splitLines(content); + expect(lines).toEqual(["A", "B", "C", "D"]); + }); + + it("should remove empty lines", () => { + const content = "A\n\n\n\nB"; + const lines = splitLines(content); + expect(lines).toEqual(["A", "B"]); + }); + }); +}); diff --git a/test/utils/zod-utils.ts b/test/unit/utils/zod-utils.ts similarity index 92% rename from test/utils/zod-utils.ts rename to test/unit/utils/zod-utils.ts index 4650cb9c..e5a6710b 100644 --- a/test/utils/zod-utils.ts +++ b/test/unit/utils/zod-utils.ts @@ -1,4 +1,4 @@ -import { removeExplicitUndefined } from "../../src/utils/zod-utils"; +import { removeExplicitUndefined } from "../../../src/utils/zod-utils"; describe("zod utils", () => { describe("remove explicit undefined", () => {