diff --git a/src/cli/cmd-add.ts b/src/cli/cmd-add.ts index e0c2c34d..488cb6d0 100644 --- a/src/cli/cmd-add.ts +++ b/src/cli/cmd-add.ts @@ -4,10 +4,7 @@ import { WriteProjectManifest, } from "../io/project-manifest-io"; import { ParseEnv } from "../services/parse-env"; -import { - compareEditorVersion, - stringifyEditorVersion, -} from "../domain/editor-version"; +import { compareEditorVersion, EditorVersion } from "../domain/editor-version"; import { DomainName } from "../domain/domain-name"; import { makePackageReference, @@ -26,56 +23,39 @@ import { UnityProjectManifest, } from "../domain/project-manifest"; import { CmdOptions } from "./options"; -import { - PackumentVersionResolveError, - pickMostFixable, -} from "../packument-version-resolving"; +import { pickMostFixable } from "../packument-version-resolving"; import { SemanticVersion } from "../domain/semantic-version"; import { areArraysEqual } from "../utils/array-utils"; -import { Err, Ok, Result } from "ts-results-es"; import { CustomError } from "ts-custom-error"; -import { - DependencyResolveError, - ResolveDependencies, -} from "../services/dependency-resolving"; +import { ResolveDependencies } from "../services/dependency-resolving"; import { ResolveRemotePackumentVersion } from "../services/resolve-remote-packument-version"; import { Logger } from "npmlog"; import { logValidDependency } from "./dependency-logging"; import { unityRegistryUrl } from "../domain/registry-url"; import { tryGetTargetEditorVersionFor } from "../domain/package-manifest"; -import { VersionNotFoundError } from "../domain/packument"; import { DebugLog } from "../logging"; import { DetermineEditorVersion } from "../services/determine-editor-version"; -import { FetchPackumentError } from "../io/packument-io"; import { ResultCodes } from "./result-codes"; -import { - notifyEnvParsingFailed, - notifyManifestLoadFailed, - notifyManifestWriteFailed, - notifyProjectVersionLoadFailed, - notifyRemotePackumentVersionResolvingFailed, -} from "./error-logging"; +import { logError } from "./error-logging"; -export class InvalidPackumentDataError extends CustomError { - private readonly _class = "InvalidPackumentDataError"; - constructor(readonly issue: string) { - super("A packument object was malformed."); +export class PackageIncompatibleError extends CustomError { + constructor( + public readonly packageRef: PackageReference, + public readonly editorVersion: EditorVersion + ) { + super(); } } -export class EditorIncompatibleError extends CustomError { - private readonly _class = "EditorIncompatibleError"; - constructor() { - super( - "A packuments target editor-version was not compatible with the installed editor-version." - ); +export class UnresolvedDependenciesError extends CustomError { + constructor(public readonly packageRef: PackageReference) { + super(); } } -export class UnresolvedDependencyError extends CustomError { - private readonly _class = "UnresolvedDependencyError"; - constructor() { - super("A packuments dependency could not be resolved."); +export class CompatibilityCheckFailedError extends CustomError { + constructor(public readonly packageRef: PackageReference) { + super(); } } @@ -84,14 +64,6 @@ export type AddOptions = CmdOptions<{ force?: boolean; }>; -type AddError = - | PackumentVersionResolveError - | FetchPackumentError - | InvalidPackumentDataError - | EditorIncompatibleError - | UnresolvedDependencyError - | DependencyResolveError; - /** * The different command result codes for the add command. */ @@ -124,19 +96,9 @@ export function makeAddCmd( if (!Array.isArray(pkgs)) pkgs = [pkgs]; // parse env - const envResult = await parseEnv(options); - if (envResult.isErr()) { - notifyEnvParsingFailed(log, envResult.error); - return ResultCodes.Error; - } - const env = envResult.value; + const env = await parseEnv(options); - const editorVersionResult = await determineEditorVersion(env.cwd).promise; - if (editorVersionResult.isErr()) { - notifyProjectVersionLoadFailed(log, editorVersionResult.error); - return ResultCodes.Error; - } - const editorVersion = editorVersionResult.value; + const editorVersion = await determineEditorVersion(env.cwd); if (typeof editorVersion === "string") log.warn( @@ -147,7 +109,7 @@ export function makeAddCmd( const tryAddToManifest = async function ( manifest: UnityProjectManifest, pkg: PackageReference - ): Promise> { + ): Promise<[UnityProjectManifest, boolean]> { // is upstream package flag let isUpstreamPackage = false; // parse name @@ -176,77 +138,48 @@ export function makeAddCmd( } } - if (resolveResult.isErr()) { - notifyRemotePackumentVersionResolvingFailed( - log, - name, - resolveResult.error - ); - return resolveResult; - } + if (resolveResult.isErr()) throw resolveResult.error; const packumentVersion = resolveResult.value.packumentVersion; versionToAdd = packumentVersion.version; - const targetEditorVersionResult = - tryGetTargetEditorVersionFor(packumentVersion); - if (targetEditorVersionResult.isErr()) { - log.warn( - "package.unity", - `${targetEditorVersionResult.error.versionString} is not valid` - ); - if (!options.force) { - log.notice( - "suggest", - "contact the package author to fix the issue, or run with option -f to ignore the warning" - ); - return Err( - new InvalidPackumentDataError("Editor-version not valid.") - ); - } - } else { - const targetEditorVersion = targetEditorVersionResult.value; - - // verify editor version - if ( - targetEditorVersion !== null && - typeof editorVersion !== "string" && - compareEditorVersion(editorVersion, targetEditorVersion) < 0 - ) { - log.warn( - "editor.version", - `requires ${targetEditorVersion} but found ${stringifyEditorVersion( - editorVersion - )}` - ); + // Only do compatibility check when we have a valid editor version + if (typeof editorVersion !== "string") { + let targetEditorVersion: EditorVersion | null; + try { + targetEditorVersion = + tryGetTargetEditorVersionFor(packumentVersion); + } catch (error) { if (!options.force) { - log.notice( - "suggest", - `upgrade the editor to ${targetEditorVersion}, or run with option -f to ignore the warning` + const packageRef = makePackageReference(name, versionToAdd); + debugLog( + `"${packageRef}" is malformed. Target editor version could not be determined.` ); - return Err(new EditorIncompatibleError()); + throw new CompatibilityCheckFailedError(packageRef); } + targetEditorVersion = null; } + + // verify editor version + const isCompatible = + targetEditorVersion === null || + compareEditorVersion(editorVersion, targetEditorVersion) >= 0; + if (!isCompatible && !options.force) + throw new PackageIncompatibleError( + makePackageReference(name, versionToAdd), + targetEditorVersion! + ); } // pkgsInScope if (!isUpstreamPackage) { debugLog(`fetch: ${makePackageReference(name, requestedVersion)}`); - const resolveResult = await resolveDependencies( + const [depsValid, depsInvalid] = await resolveDependencies( [env.registry, env.upstreamRegistry], name, requestedVersion, true ); - if (resolveResult.isErr()) { - notifyRemotePackumentVersionResolvingFailed( - log, - name, - resolveResult.error - ); - return resolveResult; - } - const [depsValid, depsInvalid] = resolveResult.value; // add depsValid to pkgsInScope. depsValid.forEach((dependency) => @@ -263,35 +196,17 @@ export function makeAddCmd( // print suggestion for depsInvalid let isAnyDependencyUnresolved = false; depsInvalid.forEach((depObj) => { - notifyRemotePackumentVersionResolvingFailed( - log, - depObj.name, - depObj.reason - ); + logError(log, depObj.reason); // If the manifest already has the dependency than it does not // really matter that it was not resolved. - if (!hasDependency(manifest, depObj.name)) { + if (!hasDependency(manifest, depObj.name)) isAnyDependencyUnresolved = true; - if (depObj.reason instanceof VersionNotFoundError) - log.notice( - "suggest", - `to install ${makePackageReference( - depObj.name, - depObj.reason.requestedVersion - )} or a replaceable version manually` - ); - } }); - if (isAnyDependencyUnresolved) { - if (!options.force) { - log.error( - "missing dependencies", - "please resolve the issue or run with option -f to ignore the warning" - ); - return Err(new UnresolvedDependencyError()); - } - } + if (isAnyDependencyUnresolved && !options.force) + throw new UnresolvedDependenciesError( + makePackageReference(name, versionToAdd) + ); } else pkgsInScope.push(name); } // add to dependencies @@ -338,24 +253,19 @@ export function makeAddCmd( } if (options.test) manifest = addTestable(manifest, name); - return Ok([manifest, dirty]); + return [manifest, dirty]; }; // load manifest - const loadResult = await loadProjectManifest(env.cwd).promise; - if (loadResult.isErr()) { - notifyManifestLoadFailed(log, loadResult.error); - return ResultCodes.Error; - } - let manifest = loadResult.value; + let manifest = await loadProjectManifest(env.cwd); // add let dirty = false; for (const pkg of pkgs) { - const result = await tryAddToManifest(manifest, pkg); - if (result.isErr()) return ResultCodes.Error; - - const [newManifest, manifestChanged] = result.value; + const [newManifest, manifestChanged] = await tryAddToManifest( + manifest, + pkg + ); if (manifestChanged) { manifest = newManifest; dirty = true; @@ -364,12 +274,7 @@ export function makeAddCmd( // Save manifest if (dirty) { - const saveResult = await writeProjectManifest(env.cwd, manifest).promise; - if (saveResult.isErr()) { - notifyManifestWriteFailed(log); - return ResultCodes.Error; - } - + await writeProjectManifest(env.cwd, manifest); // print manifest notice log.notice("", "please open Unity project to apply changes"); } diff --git a/src/cli/cmd-deps.ts b/src/cli/cmd-deps.ts index c28e8025..fab44912 100644 --- a/src/cli/cmd-deps.ts +++ b/src/cli/cmd-deps.ts @@ -6,7 +6,7 @@ import { splitPackageReference, } from "../domain/package-reference"; import { CmdOptions } from "./options"; -import { PackumentVersionResolveError } from "../packument-version-resolving"; +import { ResolvePackumentVersionError } from "../packument-version-resolving"; import { PackumentNotFoundError } from "../common-errors"; import { ResolveDependencies } from "../services/dependency-resolving"; import { Logger } from "npmlog"; @@ -14,10 +14,6 @@ import { logValidDependency } from "./dependency-logging"; import { VersionNotFoundError } from "../domain/packument"; import { DebugLog } from "../logging"; import { ResultCodes } from "./result-codes"; -import { - notifyEnvParsingFailed, - notifyRemotePackumentVersionResolvingFailed, -} from "./error-logging"; export type DepsOptions = CmdOptions<{ deep?: boolean; @@ -38,7 +34,7 @@ export type DepsCmd = ( options: DepsOptions ) => Promise; -function errorPrefixForError(error: PackumentVersionResolveError): string { +function errorPrefixForError(error: ResolvePackumentVersionError): string { if (error instanceof PackumentNotFoundError) return "missing dependency"; else if (error instanceof VersionNotFoundError) return "missing dependency version"; @@ -56,12 +52,7 @@ export function makeDepsCmd( ): DepsCmd { return async (pkg, options) => { // parse env - const envResult = await parseEnv(options); - if (envResult.isErr()) { - notifyEnvParsingFailed(log, envResult.error); - return ResultCodes.Error; - } - const env = envResult.value; + const env = await parseEnv(options); const [name, version] = splitPackageReference(pkg); @@ -72,22 +63,12 @@ export function makeDepsCmd( const deep = options.deep || false; debugLog(`fetch: ${makePackageReference(name, version)}, deep=${deep}`); - const resolveResult = await resolveDependencies( + const [depsValid, depsInvalid] = await resolveDependencies( [env.registry, env.upstreamRegistry], name, version, deep ); - if (resolveResult.isErr()) { - notifyRemotePackumentVersionResolvingFailed( - log, - name, - resolveResult.error - ); - return ResultCodes.Error; - } - - const [depsValid, depsInvalid] = resolveResult.value; depsValid.forEach((dependency) => logValidDependency(debugLog, dependency)); depsValid diff --git a/src/cli/cmd-login.ts b/src/cli/cmd-login.ts index 2034eba0..546b0c6a 100644 --- a/src/cli/cmd-login.ts +++ b/src/cli/cmd-login.ts @@ -11,8 +11,6 @@ import { CmdOptions } from "./options"; import { Logger } from "npmlog"; import { Login } from "../services/login"; import { ResultCodes } from "./result-codes"; -import { RegistryAuthenticationError } from "../io/common-errors"; -import { notifyEnvParsingFailed } from "./error-logging"; /** * Options for logging in a user. These come from the CLI. @@ -49,12 +47,7 @@ export function makeLoginCmd( ): LoginCmd { return async (options) => { // parse env - const envResult = await parseEnv(options); - if (envResult.isErr()) { - notifyEnvParsingFailed(log, envResult.error); - return ResultCodes.Error; - } - const env = envResult.value; + const env = await parseEnv(options); // query parameters const username = options.username ?? (await promptUsername()); @@ -68,15 +61,9 @@ export function makeLoginCmd( const alwaysAuth = options.alwaysAuth || false; - const configPathResult = await getUpmConfigPath(env.wsl, env.systemUser) - .promise; - if (configPathResult.isErr()) { - // TODO: Log error - return ResultCodes.Error; - } - const configPath = configPathResult.value; + const configPath = await getUpmConfigPath(env.wsl, env.systemUser); - const loginResult = await login( + await login( username, password, email, @@ -84,16 +71,7 @@ export function makeLoginCmd( loginRegistry, configPath, options.basicAuth ? "basic" : "token" - ).promise; - - if (loginResult.isErr()) { - const loginError = loginResult.error; - if (loginError instanceof RegistryAuthenticationError) - log.warn("401", "Incorrect username or password"); - - // TODO: Log all errors - return ResultCodes.Error; - } + ); log.notice("auth", `you are authenticated as '${username}'`); log.notice("config", "saved unity config at " + configPath); diff --git a/src/cli/cmd-remove.ts b/src/cli/cmd-remove.ts index 5c42f8f4..9bb00cd6 100644 --- a/src/cli/cmd-remove.ts +++ b/src/cli/cmd-remove.ts @@ -3,12 +3,9 @@ import { makePackageReference } from "../domain/package-reference"; import { CmdOptions } from "./options"; import { Logger } from "npmlog"; import { ResultCodes } from "./result-codes"; -import { - notifyEnvParsingFailed, - notifyPackageRemoveFailed, -} from "./error-logging"; import { DomainName } from "../domain/domain-name"; import { RemovePackages } from "../services/remove-packages"; +import { logError } from "./error-logging"; /** * The possible result codes with which the remove command can exit. @@ -37,16 +34,11 @@ export function makeRemoveCmd( ): RemoveCmd { return async (pkgs, options) => { // parse env - const envResult = await parseEnv(options); - if (envResult.isErr()) { - notifyEnvParsingFailed(log, envResult.error); - return ResultCodes.Error; - } - const env = envResult.value; + const env = await parseEnv(options); const removeResult = await removePackages(env.cwd, pkgs).promise; if (removeResult.isErr()) { - notifyPackageRemoveFailed(log, removeResult.error); + logError(log, removeResult.error); return ResultCodes.Error; } const removedPackages = removeResult.value; diff --git a/src/cli/cmd-search.ts b/src/cli/cmd-search.ts index c07cb63b..f6797012 100644 --- a/src/cli/cmd-search.ts +++ b/src/cli/cmd-search.ts @@ -6,7 +6,6 @@ import { Logger } from "npmlog"; import { SearchPackages } from "../services/search-packages"; import { DebugLog } from "../logging"; import { ResultCodes } from "./result-codes"; -import { notifyEnvParsingFailed } from "./error-logging"; /** * The possible result codes with which the search command can exit. @@ -36,25 +35,14 @@ export function makeSearchCmd( ): SearchCmd { return async (keyword, options) => { // parse env - const envResult = await parseEnv(options); - if (envResult.isErr()) { - notifyEnvParsingFailed(log, envResult.error); - return ResultCodes.Error; - } - const env = envResult.value; + const env = await parseEnv(options); let usedEndpoint = "npmsearch"; - const searchResult = await searchPackages(env.registry, keyword, () => { + const results = await searchPackages(env.registry, keyword, () => { usedEndpoint = "endpoint.all"; log.warn("", "fast search endpoint is not available, using old search."); - }).promise; - - if (searchResult.isErr()) { - log.warn("", "/-/all endpoint is not available"); - return ResultCodes.Error; - } + }); - const results = searchResult.value; if (results.length === 0) { log.notice("", `No matches found for "${keyword}"`); return ResultCodes.Ok; diff --git a/src/cli/cmd-view.ts b/src/cli/cmd-view.ts index e728d67d..b2269e8e 100644 --- a/src/cli/cmd-view.ts +++ b/src/cli/cmd-view.ts @@ -11,9 +11,9 @@ import { CmdOptions } from "./options"; import { recordKeys } from "../utils/record-utils"; import { Logger } from "npmlog"; import { ResultCodes } from "./result-codes"; -import { notifyEnvParsingFailed } from "./error-logging"; import { FetchPackument } from "../io/packument-io"; import { queryAllRegistriesLazy } from "../utils/sources"; +import { PackumentNotFoundError } from "../common-errors"; export type ViewOptions = CmdOptions; @@ -111,12 +111,7 @@ export function makeViewCmd( ): ViewCmd { return async (pkg, options) => { // parse env - const envResult = await parseEnv(options); - if (envResult.isErr()) { - notifyEnvParsingFailed(log, envResult.error); - return ResultCodes.Error; - } - const env = envResult.value; + const env = await parseEnv(options); // parse name if (hasVersion(pkg)) { @@ -130,19 +125,12 @@ export function makeViewCmd( env.registry, ...(env.upstream ? [env.upstreamRegistry] : []), ]; - const resolveResult = await queryAllRegistriesLazy(sources, (source) => - fetchPackument(source, pkg) - ).promise; - if (!resolveResult.isOk()) { - // TODO: Print error - return ResultCodes.Error; - } - - const packument = resolveResult.value?.value ?? null; - if (packument === null) { - log.error("404", `package not found: ${pkg}`); - return ResultCodes.Error; - } + const packumentFromRegistry = await queryAllRegistriesLazy( + sources, + (source) => fetchPackument(source, pkg) + ); + const packument = packumentFromRegistry?.value ?? null; + if (packument === null) throw new PackumentNotFoundError(pkg); printInfo(packument); return ResultCodes.Ok; diff --git a/src/cli/error-logging.ts b/src/cli/error-logging.ts index 71755b25..a81df7da 100644 --- a/src/cli/error-logging.ts +++ b/src/cli/error-logging.ts @@ -1,271 +1,145 @@ import { Logger } from "npmlog"; -import { EnvParseError } from "../services/parse-env"; +import { ResultCodes } from "./result-codes"; +import { RegistryAuthLoadError } from "../services/parse-env"; import { NoWslError } from "../io/wsl"; -import { ChildProcessError } from "../io/child-process"; -import { RequiredEnvMissingError } from "../io/upm-config-io"; import { - FileMissingError, - FileParseError, - GenericIOError, - GenericNetworkError, - RegistryAuthenticationError, -} from "../io/common-errors"; -import { StringFormatError } from "../utils/string-parsing"; -import { ProjectVersionLoadError } from "../io/project-version-io"; -import { PackumentNotFoundError } from "../common-errors"; -import { DomainName } from "../domain/domain-name"; + 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 { SemanticVersion } from "../domain/semantic-version"; import { EOL } from "node:os"; -import { ResolveRemotePackumentVersionError } from "../services/resolve-remote-packument-version"; -import { ManifestLoadError } from "../io/project-manifest-io"; -import { RemovePackagesError } from "../services/remove-packages"; - -export function suggestCheckingWorkingDirectory(log: Logger) { - log.notice("", "Are you in the correct working directory?"); -} - -export function notifyManifestMissing(log: Logger, filePath: string) { - log.error("", `Could not locate your project manifest at "${filePath}".`); - suggestCheckingWorkingDirectory(log); -} - -export function suggestFixErrorsInProjectManifest(log: Logger) { - log.notice( - "", - "Please fix the errors in your project manifest and try again." - ); -} - -export function notifySyntacticallyMalformedProjectManifest(log: Logger) { - log.error("", "Project manifest file contained json syntax errors."); - suggestFixErrorsInProjectManifest(log); -} - -export function notifySemanticallyMalformedProjectManifest(log: Logger) { - log.error( - "", - "Project manifest is valid json but was not of the correct shape." - ); - suggestFixErrorsInProjectManifest(log); -} - -export function notifyManifestLoadFailedBecauseIO(log: Logger) { - log.error( - "", - "Could not load project manifest because of a file-system error." - ); -} - -export function notifyManifestLoadFailed( - log: Logger, - error: ManifestLoadError -) { - if (error instanceof FileMissingError) notifyManifestMissing(log, error.path); - else if (error instanceof StringFormatError) - notifySyntacticallyMalformedProjectManifest(log); - else if (error instanceof FileParseError) - notifySemanticallyMalformedProjectManifest(log); - else if (error instanceof GenericIOError) - notifyManifestLoadFailedBecauseIO(log); -} - -export function notifyManifestWriteFailed(log: Logger) { - log.error( - "", - "Could not save project manifest because of a file-system error." - ); -} - -export function notifyNotUsingWsl(log: Logger) { - log.error("", "No wsl detected."); - log.notice("", "Please make sure you are actually running openupm in wsl."); -} - -export function notifyChildProcessError(log: Logger) { - log.error("", "A child process encountered an error."); -} - -export function notifyMissingEnvForUpmConfigPath( - log: Logger, - variableNames: string[] -) { - const nameList = variableNames.map((name) => `"${name}"`).join(", "); - log.error( - "", - "Could not determine upm-config path because of missing home environment variables." - ); - log.notice( - "", - `Please make sure that you set one of the following environment variables: ${nameList}.` - ); -} - -export function notifySyntacticallyMalformedUpmConfig(log: Logger) { - log.error("", "Upm-config file contained toml syntax errors."); - log.notice("", "Please fix the errors in your upm-config and try again."); -} - -function notifyUpmConfigLoadFailedBecauseIO(log: Logger) { - log.error("", "Could not load upm-config because of a file-system error."); -} - -export function notifyEnvParsingFailed(log: Logger, error: EnvParseError) { - if (error instanceof NoWslError) notifyNotUsingWsl(log); - else if (error instanceof ChildProcessError) notifyChildProcessError(log); - else if (error instanceof RequiredEnvMissingError) - notifyMissingEnvForUpmConfigPath(log, error.keyNames); - else if (error instanceof GenericIOError) - notifyUpmConfigLoadFailedBecauseIO(log); - else if (error instanceof StringFormatError) - notifySyntacticallyMalformedUpmConfig(log); -} - -export function notifyProjectVersionMissing(log: Logger, filePath: string) { - log.error( - "", - `Could not locate your projects version file (ProjectVersion.txt) at "${filePath}".` - ); - suggestCheckingWorkingDirectory(log); -} - -export function suggestFixErrorsInProjectVersionFile(log: Logger) { - log.notice( - "", - "Please fix the errors in your project version file and try again." - ); -} - -export function notifySyntacticallyMalformedProjectVersion(log: Logger) { - log.error( - "", - "Project version file (ProjectVersion.txt) file contained yaml syntax errors." - ); - suggestFixErrorsInProjectVersionFile(log); -} - -export function notifySemanticallyMalformedProjectVersion(log: Logger) { - log.error( - "", - "Project version file (ProjectVersion.txt) file is valid yaml but was not of the correct shape." - ); - suggestFixErrorsInProjectVersionFile(log); -} - -export function notifyProjectVersionLoadFailed( - log: Logger, - error: ProjectVersionLoadError -) { - if (error instanceof FileMissingError) - notifyProjectVersionMissing(log, error.path); - else if (error instanceof GenericIOError) - log.error( - "", - "Could not load project version file (ProjectVersion.txt) because of a file-system error." - ); - else if (error instanceof StringFormatError) - notifySyntacticallyMalformedProjectVersion(log); - else if (error instanceof FileParseError) - notifySemanticallyMalformedProjectVersion(log); -} - -export function notifyPackumentNotFoundInAnyRegistry( - log: Logger, - packageName: DomainName -) { - log.error( - "", - `The package "${packageName}" was not found in any of the provided registries.` - ); - log.notice( - "", - "Please make sure you have spelled the name and registry urls correctly." - ); -} - -export function notifyPackumentNotFoundInManifest( - log: Logger, - packageName: DomainName -) { - log.error( - "", - `The package "${packageName}" was not found in your project manifest.` - ); - log.notice("", "Please make sure you have spelled the name correctly."); -} - -export function notifyNoVersions(log: Logger, packageName: DomainName) { - log.error("", `The package ${packageName} has no versions.`); -} - -export function notifyOfMissingVersion( - log: Logger, - packageName: DomainName, - requestedVersion: SemanticVersion, - availableVersions: ReadonlyArray -) { - const versionList = availableVersions - .map((version) => `\t- ${version}`) - .join(EOL); - - log.error( - "", - `The package "${packageName}" has no published version "${requestedVersion}".` - ); - log.notice("", `Maybe you meant one of the following:${EOL}${versionList}`); -} - -export function notifyRegistryCallFailedBecauseHttp(log: Logger) { - log.error( - "", - "Could not communicate with registry because of an http error." - ); -} - -export function notifyRegistryCallFailedBecauseUnauthorized(log: Logger) { - log.error( - "", - "An npm registry rejected your request, because you are unauthorized." - ); - log.notice( - "", - "Please make sure you are correctly authenticated for the registry." - ); -} - -export function notifyRemotePackumentVersionResolvingFailed( - log: Logger, - packageName: DomainName, - error: ResolveRemotePackumentVersionError -) { +import { EditorNotInstalledError } from "../io/builtin-packages"; +import { RegistryAuthenticationError } from "../io/common-errors"; +import { + NoHomePathError, + OSNotSupportedError, + VersionNotSupportedOnOsError, +} from "../io/special-paths"; +import { + ManifestMalformedError, + ManifestMissingError, +} from "../io/project-manifest-io"; +import { + ProjectVersionMalformedError, + ProjectVersionMissingError, +} from "../io/project-version-io"; +import { NoSystemUserProfilePath } from "../io/upm-config-io"; + +function makeErrorMessageFor(error: unknown): string { + if (error instanceof RegistryAuthLoadError) + return "Could not load registry authentication information."; + if (error instanceof NoWslError) return "Not running in wsl."; if (error instanceof PackumentNotFoundError) - notifyPackumentNotFoundInAnyRegistry(log, packageName); - else if (error instanceof NoVersionsError) notifyNoVersions(log, packageName); - else if (error instanceof VersionNotFoundError) - notifyOfMissingVersion( - log, - packageName, - error.requestedVersion, - error.availableVersions - ); - else if (error instanceof GenericNetworkError) - notifyRegistryCallFailedBecauseHttp(log); - else if (error instanceof RegistryAuthenticationError) - notifyRegistryCallFailedBecauseUnauthorized(log); -} - -export function notifyPackageRemoveFailed( + return `Package "${error.packageName}" could not be found.`; + if (error instanceof EditorVersionNotSupportedError) + return `OpenUPM is not compatible with Unity ${stringifyEditorVersion( + error.version + )}.`; + if (error instanceof CompatibilityCheckFailedError) + return `Could not confirm editor compatibility for ${error.packageRef}.`; + if (error instanceof PackageIncompatibleError) + return `"${ + error.packageRef + }" is not compatible with Unity ${stringifyEditorVersion( + error.editorVersion + )}.`; + if (error instanceof UnresolvedDependenciesError) + return `"${error.packageRef}" has one or more unresolved dependencies and was not added.`; + if (error instanceof NoVersionsError) + return `"${error.packageName}" can not be added because it has no published versions.`; + if (error instanceof VersionNotFoundError) + return `Can not add "${error.packageName}" because version ${error.requestedVersion} could not be found in any registry.`; + if (error instanceof EditorNotInstalledError) + return "Your projects Unity editor version is not installed."; + if (error instanceof RegistryAuthenticationError) + return `The registry at "${error.registryUrl}" refused your request because you are not authenticated.`; + if (error instanceof VersionNotSupportedOnOsError) + return "Your projects Unity editor version is not supported on your OS."; + if (error instanceof OSNotSupportedError) + return "Unity does not support your OS."; + if (error instanceof ManifestMissingError) + return `Project manifest could not be found at "${error.expectedPath}".`; + if (error instanceof ManifestMalformedError) + return "Project manifest could not be parsed because it has malformed content."; + if (error instanceof ProjectVersionMissingError) + return `Project version could not be found at "${error.expectedPath}".`; + if (error instanceof ProjectVersionMalformedError) + return "Project version could not be parsed because it has malformed content."; + if (error instanceof NoHomePathError) + return "Could not determine path of home directory."; + if (error instanceof NoSystemUserProfilePath) + return "Could not determine path of system user directory."; + return "A fatal error occurred."; +} + +function tryMakeFixSuggestionFor(error: unknown): string | null { + if (error instanceof RegistryAuthLoadError) + return "Most likely this means that something is wrong with your .upmconfig.toml."; + if (error instanceof NoWslError) + return "Please make sure you are running in wsl, or remove the --wsl flag."; + if (error instanceof PackumentNotFoundError) + return "Did you make a typo when spelling the name?"; + if (error instanceof CompatibilityCheckFailedError) + return `Fix the issue or run with --force to add anyway.`; + if (error instanceof PackageIncompatibleError) + return "Add a different version or run with --force to add anyway."; + if (error instanceof UnresolvedDependenciesError) + return "Resolve the dependency issues or run with --force to add anyway."; + if (error instanceof VersionNotFoundError) + return `Make sure you chose the right version. Here is a list of your options:${EOL}${error.availableVersions.join( + ", " + )}`; + if (error instanceof EditorNotInstalledError) + return `Make sure you have ${stringifyEditorVersion( + error.version + )} installed.`; + if (error instanceof RegistryAuthenticationError) + return `Use "openupm login" to authenticate with the registry.`; + if ( + error instanceof ManifestMissingError || + error instanceof ProjectVersionMissingError + ) + return "Check if you are in the correct working directory."; + if ( + error instanceof ManifestMalformedError || + error instanceof ProjectVersionMalformedError + ) + return "Please fix any format errors and try again."; + if (error instanceof NoHomePathError) + return "Make sure you run OpenUPM with either the HOME or USERPROFILE environment variable set to your home path."; + if (error instanceof NoSystemUserProfilePath) + return "Make sure you run OpenUPM with the ALLUSERSPROFILE environment variable set to your home path."; + return null; +} + +export function logError(log: Logger, error: unknown) { + const message = makeErrorMessageFor(error); + log.error("", message); + + const fixSuggestion = tryMakeFixSuggestionFor(error); + if (fixSuggestion !== null) log.notice("", fixSuggestion); + + const isLoggingVerbose = log.level === "verbose" || log.level === "silly"; + if (!isLoggingVerbose) + log.notice("", "Run with --verbose to get more information."); +} + +export function withErrorLogger< + TArgs extends unknown[], + TOut extends ResultCodes +>( log: Logger, - error: RemovePackagesError -) { - if (error instanceof FileMissingError) notifyManifestMissing(log, error.path); - else if (error instanceof StringFormatError) - notifySyntacticallyMalformedProjectManifest(log); - else if (error instanceof FileParseError) - notifySemanticallyMalformedProjectManifest(log); - else if (error instanceof GenericIOError) { - if (error.operationType === "Read") notifyManifestLoadFailedBecauseIO(log); - else notifyManifestWriteFailed(log); - } else if (error instanceof PackumentNotFoundError) - notifyPackumentNotFoundInManifest(log, error.packageName); + cmd: (...args: TArgs) => Promise +): (...args: TArgs) => Promise { + return (...args) => + cmd(...args).catch((error) => { + logError(log, error); + process.exit(ResultCodes.Error); + }); } diff --git a/src/cli/index.ts b/src/cli/index.ts index f58fd642..60418c37 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -34,7 +34,7 @@ import { makeLoadUpmConfig, makeSaveUpmConfig, } from "../io/upm-config-io"; -import { makeReadText, makeWriteText } from "../io/fs-result"; +import { makeReadText, makeWriteText } from "../io/text-file-io"; import { makeFindNpmrcPath, makeLoadNpmrc, @@ -53,24 +53,27 @@ import { makeRunChildProcess } from "../io/child-process"; import { makeCheckIsBuiltInPackage } from "../services/built-in-package-check"; import { makeCheckIsUnityPackage } from "../services/unity-package-check"; import { makeCheckUrlExists } from "../io/check-url"; +import { withErrorLogger } from "./error-logging"; // Composition root const log = npmlog; const debugLog: DebugLog = (message, context) => log.verbose( - "openupm-cli", + "", `${message}${ - context !== undefined ? ` context: ${JSON.stringify(context)}` : "" + context !== undefined + ? ` context: ${JSON.stringify(context, null, 2)}` + : "" }` ); const regClient = new RegClient({ log }); const getCwd = makeGetCwd(); const runChildProcess = makeRunChildProcess(debugLog); const getHomePath = makeGetHomePath(); -const readFile = makeReadText(debugLog); -const writeFile = makeWriteText(debugLog); -const loadProjectManifest = makeLoadProjectManifest(readFile); +const readFile = makeReadText(); +const writeFile = makeWriteText(); +const loadProjectManifest = makeLoadProjectManifest(readFile, debugLog); const writeProjectManifest = makeWriteProjectManifest(writeFile); const getUpmConfigPath = makeGetUpmConfigPath(getHomePath, runChildProcess); const loadUpmConfig = makeLoadUpmConfig(readFile); @@ -78,8 +81,8 @@ const saveUpmConfig = makeSaveUpmConfig(writeFile); const findNpmrcPath = makeFindNpmrcPath(getHomePath); const loadNpmrc = makeLoadNpmrc(readFile); const saveNpmrc = makeSaveNpmrc(writeFile); -const loadProjectVersion = makeLoadProjectVersion(readFile); -const fetchPackument = makeFetchPackument(regClient); +const loadProjectVersion = makeLoadProjectVersion(readFile, debugLog); +const fetchPackument = makeFetchPackument(regClient, debugLog); const fetchAllPackuments = makeFetchAllPackuments(debugLog); const searchRegistry = makeSearchRegistry(debugLog); const removePackages = makeRemovePackages( @@ -88,7 +91,13 @@ const removePackages = makeRemovePackages( ); const checkUrlExists = makeCheckUrlExists(); -const parseEnv = makeParseEnv(log, getUpmConfigPath, loadUpmConfig, getCwd); +const parseEnv = makeParseEnv( + log, + getUpmConfigPath, + loadUpmConfig, + getCwd, + debugLog +); const determineEditorVersion = makeDetermineEditorVersion(loadProjectVersion); const authNpmrc = makeAuthNpmrc(findNpmrcPath, loadNpmrc, saveNpmrc); const npmLogin = makeNpmLogin(regClient, debugLog); @@ -109,7 +118,11 @@ const saveAuthToUpmConfig = makeSaveAuthToUpmConfig( loadUpmConfig, saveUpmConfig ); -const searchPackages = makeSearchPackages(searchRegistry, fetchAllPackuments); +const searchPackages = makeSearchPackages( + searchRegistry, + fetchAllPackuments, + debugLog +); const login = makeLogin(saveAuthToUpmConfig, npmLogin, authNpmrc, debugLog); const addCmd = makeAddCmd( @@ -178,11 +191,13 @@ program openupm add [otherPkgs...] openupm add @ [otherPkgs...]` ) - .action(async function (pkg, otherPkgs, options) { - const pkgs = [pkg].concat(otherPkgs); - const resultCode = await addCmd(pkgs, makeCmdOptions(options)); - process.exit(resultCode); - }); + .action( + withErrorLogger(log, async function (pkg, otherPkgs, options) { + const pkgs = [pkg].concat(otherPkgs); + const resultCode = await addCmd(pkgs, makeCmdOptions(options)); + process.exit(resultCode); + }) + ); program .command("remove") @@ -194,31 +209,43 @@ program ) .aliases(["rm", "uninstall"]) .description("remove package from manifest json") - .action(async function (packageName, otherPackageNames, options) { - const packageNames = [packageName].concat(otherPackageNames); - const resultCode = await removeCmd(packageNames, makeCmdOptions(options)); - process.exit(resultCode); - }); + .action( + withErrorLogger( + log, + async function (packageName, otherPackageNames, options) { + const packageNames = [packageName].concat(otherPackageNames); + const resultCode = await removeCmd( + packageNames, + makeCmdOptions(options) + ); + process.exit(resultCode); + } + ) + ); program .command("search") .argument("", "The keyword to search") .aliases(["s", "se", "find"]) .description("Search package by keyword") - .action(async function (keyword, options) { - const resultCode = await searchCmd(keyword, makeCmdOptions(options)); - process.exit(resultCode); - }); + .action( + withErrorLogger(log, async function (keyword, options) { + const resultCode = await searchCmd(keyword, makeCmdOptions(options)); + process.exit(resultCode); + }) + ); program .command("view") .argument("", "Reference to a package", mustBePackageReference) .aliases(["v", "info", "show"]) .description("view package information") - .action(async function (pkg, options) { - const resultCode = await viewCmd(pkg, makeCmdOptions(options)); - process.exit(resultCode); - }); + .action( + withErrorLogger(log, async function (pkg, options) { + const resultCode = await viewCmd(pkg, makeCmdOptions(options)); + process.exit(resultCode); + }) + ); program .command("deps") @@ -230,10 +257,12 @@ program openupm deps openupm deps @` ) - .action(async function (pkg, options) { - const resultCode = await depsCmd(pkg, makeCmdOptions(options)); - process.exit(resultCode); - }); + .action( + withErrorLogger(log, async function (pkg, options) { + const resultCode = await depsCmd(pkg, makeCmdOptions(options)); + process.exit(resultCode); + }) + ); program .command("login") @@ -247,10 +276,12 @@ program "always auth for tarball hosted on a different domain" ) .description("authenticate with a scoped registry") - .action(async function (options) { - const resultCode = await loginCmd(makeCmdOptions(options)); - process.exit(resultCode); - }); + .action( + withErrorLogger(log, async function (options) { + const resultCode = await loginCmd(makeCmdOptions(options)); + process.exit(resultCode); + }) + ); // prompt for invalid command program.on("command:*", function () { diff --git a/src/common-errors.ts b/src/common-errors.ts index 1ceee6ea..7d5f42cb 100644 --- a/src/common-errors.ts +++ b/src/common-errors.ts @@ -7,14 +7,14 @@ import { DomainName } from "./domain/domain-name"; */ export class PackumentNotFoundError extends CustomError { // noinspection JSUnusedLocalSymbols - private readonly _class = "PackumentNotFoundError"; + constructor( /** * The name of the missing package. */ public packageName: DomainName ) { - super("A packument was not found."); + super(); } } @@ -22,7 +22,6 @@ export class PackumentNotFoundError extends CustomError { * Error for when OpenUPM was used with an editor-version that is not supported. */ export class EditorVersionNotSupportedError extends CustomError { - private readonly _class = "EditorVersionNotSupportedError"; constructor( /** * The unsupported version. @@ -32,3 +31,9 @@ export class EditorVersionNotSupportedError extends CustomError { super(); } } + +export class MalformedPackumentError extends CustomError { + constructor() { + super(); + } +} diff --git a/src/domain/package-manifest.ts b/src/domain/package-manifest.ts index 0369b6d0..dfb212fa 100644 --- a/src/domain/package-manifest.ts +++ b/src/domain/package-manifest.ts @@ -2,9 +2,9 @@ import { DomainName } from "./domain-name"; import { SemanticVersion } from "./semantic-version"; import { Maintainer } from "@npm/types"; import { recordEntries } from "../utils/record-utils"; -import { Err, Ok, Result } from "ts-results-es"; import { EditorVersion, tryParseEditorVersion } from "./editor-version"; -import { CustomError } from "ts-custom-error"; + +import { MalformedPackumentError } from "../common-errors"; type MajorMinor = `${number}.${number}`; @@ -122,23 +122,6 @@ export function dependenciesOf( return recordEntries(packageManifest["dependencies"] || {}); } -/** - * Error for when a packument contained an invalid target editor-version. - */ -export class InvalidTargetEditorError extends CustomError { - // noinspection JSUnusedLocalSymbols - private readonly _class = "InvalidTargetEditorError"; - - constructor( - /** - * The invalid editor-version string. - */ - public readonly versionString: string - ) { - super(); - } -} - /** * Extracts the target editor-version from a package-manifest. * @param packageManifest The manifest for which to get the editor. @@ -147,8 +130,8 @@ export class InvalidTargetEditorError extends CustomError { */ export function tryGetTargetEditorVersionFor( packageManifest: Pick -): Result { - if (packageManifest.unity === undefined) return Ok(null); +): EditorVersion | null { + if (packageManifest.unity === undefined) return null; const majorMinor = packageManifest.unity; const release = @@ -157,7 +140,8 @@ export function tryGetTargetEditorVersionFor( : ""; const versionString = `${majorMinor}${release}`; const parsed = tryParseEditorVersion(versionString); - return parsed !== null - ? Ok(parsed) - : Err(new InvalidTargetEditorError(versionString)); + + if (parsed === null) throw new MalformedPackumentError(); + + return parsed; } diff --git a/src/domain/packument.ts b/src/domain/packument.ts index 962eab35..f6518b1b 100644 --- a/src/domain/packument.ts +++ b/src/domain/packument.ts @@ -126,10 +126,8 @@ export function tryGetPackumentVersion( * had no versions. */ export class NoVersionsError extends CustomError { - private readonly _class = "NoVersionsError"; - - constructor() { - super("A packument contained no versions"); + constructor(public readonly packageName: DomainName) { + super(); } } @@ -138,19 +136,15 @@ export class NoVersionsError extends CustomError { * requested version did not exist. */ export class VersionNotFoundError extends CustomError { - private readonly _class = "VersionNotFoundError"; - constructor( - /** - * The version that was requested. - */ - readonly requestedVersion: SemanticVersion, + public readonly packageName: DomainName, + public readonly requestedVersion: SemanticVersion, /** * A list of available versions. */ - readonly availableVersions: ReadonlyArray + public readonly availableVersions: ReadonlyArray ) { - super("The requested version was not in the packument."); + super(); } } @@ -166,17 +160,17 @@ export class VersionNotFoundError extends CustomError { export function tryResolvePackumentVersion( packument: UnityPackument, requestedVersion: undefined | "latest" -): Result; +): Result; export function tryResolvePackumentVersion( packument: UnityPackument, requestedVersion: ResolvableVersion -): Result; +): Result; export function tryResolvePackumentVersion( packument: UnityPackument, requestedVersion: ResolvableVersion ) { const availableVersions = recordKeys(packument.versions); - if (availableVersions.length === 0) return Err(new NoVersionsError()); + if (availableVersions.length === 0) throw new NoVersionsError(packument.name); // Find the latest version if (requestedVersion === undefined || requestedVersion === "latest") { @@ -187,7 +181,13 @@ export function tryResolvePackumentVersion( // Find a specific version if (!availableVersions.includes(requestedVersion)) - return Err(new VersionNotFoundError(requestedVersion, availableVersions)); + return Err( + new VersionNotFoundError( + packument.name, + requestedVersion, + availableVersions + ) + ); return Ok(tryGetPackumentVersion(packument, requestedVersion)!); } diff --git a/src/io/all-packuments-io.ts b/src/io/all-packuments-io.ts index e9c09378..26c9f6ec 100644 --- a/src/io/all-packuments-io.ts +++ b/src/io/all-packuments-io.ts @@ -1,13 +1,9 @@ import { Registry } from "../domain/registry"; -import { AsyncResult, Err, Ok } from "ts-results-es"; import npmFetch from "npm-registry-fetch"; -import { assertIsHttpError } from "../utils/error-type-guards"; +import { assertIsError, isHttpError } from "../utils/error-type-guards"; import { getNpmFetchOptions, SearchedPackument } from "./npm-search"; import { DomainName } from "../domain/domain-name"; -import { - GenericNetworkError, - RegistryAuthenticationError, -} from "./common-errors"; +import { RegistryAuthenticationError } from "./common-errors"; import { DebugLog } from "../logging"; /** @@ -18,39 +14,33 @@ export type AllPackuments = Readonly<{ [name: DomainName]: SearchedPackument; }>; -/** - * Error for when the request to get all packuments failed. - */ -export type FetchAllPackumentsError = - | GenericNetworkError - | RegistryAuthenticationError; - /** * Function for getting fetching packuments from a npm registry. * @param registry The registry to get packuments for. */ -export type FetchAllPackuments = ( - registry: Registry -) => AsyncResult; +export type FetchAllPackuments = (registry: Registry) => Promise; /** * Makes a {@link FetchAllPackuments} function. */ export function makeFetchAllPackuments(debugLog: DebugLog): FetchAllPackuments { - return (registry) => { - return new AsyncResult( - npmFetch - .json("/-/all", getNpmFetchOptions(registry)) - .then((result) => Ok(result as AllPackuments)) - .catch((error) => { - assertIsHttpError(error); - debugLog("A http request failed.", error); - return Err( - error.statusCode === 401 - ? new RegistryAuthenticationError() - : new GenericNetworkError() - ); - }) - ); + return async (registry) => { + debugLog(`Getting all packages from ${registry.url}.`); + try { + const result = await npmFetch.json( + "/-/all", + getNpmFetchOptions(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; + } }; } diff --git a/src/io/builtin-packages.ts b/src/io/builtin-packages.ts index ad57eb77..afdb1a98 100644 --- a/src/io/builtin-packages.ts +++ b/src/io/builtin-packages.ts @@ -7,15 +7,15 @@ import { import { DomainName } from "../domain/domain-name"; import { CustomError } from "ts-custom-error"; import path from "path"; -import { tryGetDirectoriesIn } from "./fs-result"; import { DebugLog } from "../logging"; -import { GenericIOError } from "./common-errors"; +import { tryGetDirectoriesIn } from "./directory-io"; +import { assertIsNodeError } from "../utils/error-type-guards"; +import { resultifyAsyncOp } from "../utils/result-utils"; /** * Error for when an editor-version is not installed. */ export class EditorNotInstalledError extends CustomError { - private readonly _class = "EditorNotInstalledError"; constructor( /** * The version that is not installed. @@ -31,8 +31,7 @@ export class EditorNotInstalledError extends CustomError { */ export type FindBuiltInPackagesError = | GetEditorInstallPathError - | EditorNotInstalledError - | GenericIOError; + | EditorNotInstalledError; /** * Function for loading all built-in packages for an installed editor. @@ -59,14 +58,22 @@ export function makeFindBuiltInPackages( ); return ( - tryGetDirectoriesIn(packagesDir, debugLog) + resultifyAsyncOp(tryGetDirectoriesIn(packagesDir)) // We can assume correct format .map((names) => names as DomainName[]) - .mapErr((error) => - error.code === "ENOENT" - ? new EditorNotInstalledError(editorVersion) - : new GenericIOError("Read") - ) + .mapErr((error) => { + assertIsNodeError(error); + + if (error.code === "ENOENT") { + debugLog( + "Failed to get directories in built-in package directory", + error + ); + return new EditorNotInstalledError(editorVersion); + } + + throw error; + }) ); } }; diff --git a/src/io/check-url.ts b/src/io/check-url.ts index 570cb1e1..ba735c4e 100644 --- a/src/io/check-url.ts +++ b/src/io/check-url.ts @@ -1,28 +1,18 @@ -import { AsyncResult, Err, Ok, Result } from "ts-results-es"; -import { GenericNetworkError } from "./common-errors"; import fetch from "node-fetch"; /** * Function for checking if an url exists. * @param url The url to check. + * @returns A boolean indicating whether the url exists. */ -export type CheckUrlExists = ( - url: string -) => AsyncResult; +export type CheckUrlExists = (url: string) => Promise; /** * Makes a {@link CheckUrlExists} function. */ export function makeCheckUrlExists(): CheckUrlExists { - return (url) => { - return new AsyncResult( - Result.wrapAsync(() => fetch(url, { method: "HEAD" })) - ) - .mapErr(() => new GenericNetworkError()) - .andThen((reponse) => { - if (reponse.status === 200) return Ok(true); - else if (reponse.status === 404) return Ok(false); - else return Err(new GenericNetworkError()); - }); + return async (url) => { + const response = await fetch(url, { method: "HEAD" }); + return response.status === 200; }; } diff --git a/src/io/child-process.ts b/src/io/child-process.ts index deaa32b0..734605a2 100644 --- a/src/io/child-process.ts +++ b/src/io/child-process.ts @@ -1,47 +1,27 @@ import childProcess from "child_process"; -import { AsyncResult, Err, Ok } from "ts-results-es"; -import { CustomError } from "ts-custom-error"; import { DebugLog } from "../logging"; -/** - * Error that might occur when running a child process. - */ -export class ChildProcessError extends CustomError { - private readonly _class = "ChildProcessError"; - constructor() { - super(); - } -} - /** * Function that run a child process. * @param command The command to run. * @returns The commands standard output. */ -export type RunChildProcess = ( - command: string -) => AsyncResult; +export type RunChildProcess = (command: string) => Promise; /** * Makes a {@link RunChildProcess} function. */ export function makeRunChildProcess(debugLog: DebugLog): RunChildProcess { return (command) => - new AsyncResult( - new Promise(function (resolve) { - childProcess.exec(command, function (error, stdout, stderr) { - if (error) { - debugLog("A child process failed.", error); - resolve(Err(new ChildProcessError())); - return; - } - if (stderr) { - debugLog(`A child process failed with the output: ${stderr}`); - resolve(Err(new ChildProcessError())); - return; - } - resolve(Ok(stdout)); - }); - }) - ); + new Promise(function (resolve, reject) { + childProcess.exec(command, function (error, stdout, stderr) { + if (error) { + debugLog("A child process failed.", error); + reject(error); + return; + } + + resolve(stdout); + }); + }); } diff --git a/src/io/common-errors.ts b/src/io/common-errors.ts index f85c4110..e1bb524f 100644 --- a/src/io/common-errors.ts +++ b/src/io/common-errors.ts @@ -1,81 +1,11 @@ import { CustomError } from "ts-custom-error"; - -/** - * Generic error for when some non-specific IO operation failed. - */ -export class GenericIOError extends CustomError { - // noinspection JSUnusedLocalSymbols - private readonly _class = "GenericIOError"; - - public constructor( - /** - * The type of operation that failed. Either a read or write operation. - */ - public readonly operationType: "Read" | "Write" - ) { - super(); - } -} - -/** - * Error for when a required file or directory is missing. - */ -export class FileMissingError extends CustomError { - // noinspection JSUnusedLocalSymbols - private readonly _class = "FileMissingError"; - - public constructor( - /** - * Name of the file. This is a constant string that can be used in - * ifs and switches to identify the file that was missing. - */ - public readonly fileName: TName, - /** - * The path where the file was searched but not found. - */ - public readonly path: string - ) { - super(); - } -} - -/** - * Generic error for when some non-specific network operation failed. - */ -export class GenericNetworkError extends CustomError { - private readonly _class = "GenericNetworkError"; -} +import { RegistryUrl } from "../domain/registry-url"; /** * Error for when authentication with a registry failed. */ export class RegistryAuthenticationError extends CustomError { - private readonly _class = "RegistryAuthenticationError"; - - constructor() { + constructor(public readonly registryUrl: RegistryUrl) { super(); } } - -/** - * Error for when a file could not be parsed into a specific target type. - */ -export class FileParseError extends CustomError { - // noinspection JSUnusedLocalSymbols - private readonly _class = "FileParseError"; - - constructor( - /** - * The path to the file that could not be parsed. - */ - public readonly filePath: string, - /** - * A description or name of the thing that the file was supposed to be - * parsed to. This should be a constant string that can be used in ifs - * and switches. - */ - public readonly targetDescription: TTarget - ) { - super("A file could not be parsed into a specific target type."); - } -} diff --git a/src/io/directory-io.ts b/src/io/directory-io.ts new file mode 100644 index 00000000..8fca7be6 --- /dev/null +++ b/src/io/directory-io.ts @@ -0,0 +1,14 @@ +import fs from "fs/promises"; + +/** + * Attempts to get the names of all directories in a directory. + * @param directoryPath The directories name. + * TODO: Convert to service function. + */ +export async function tryGetDirectoriesIn( + directoryPath: string +): Promise> { + const entries = await fs.readdir(directoryPath, { withFileTypes: true }); + const directories = entries.filter((it) => it.isDirectory()); + return directories.map((it) => it.name); +} diff --git a/src/io/fs-result.ts b/src/io/fs-result.ts deleted file mode 100644 index 297680be..00000000 --- a/src/io/fs-result.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { AsyncResult, Result } from "ts-results-es"; -import fs from "fs/promises"; -import { assertIsNodeError } from "../utils/error-type-guards"; -import fse from "fs-extra"; -import path from "path"; -import { DebugLog } from "../logging"; - -function resultifyFsOp( - debugLog: DebugLog, - op: () => Promise -): AsyncResult { - return new AsyncResult(Result.wrapAsync(op)).mapErr((error) => { - assertIsNodeError(error); - debugLog("fs-operation failed.", error); - return error; - }); -} - -/** - * Function for loading the content of a text file. - * @param path The path to the file. - * @returns The files text content. - */ -export type ReadTextFile = ( - path: string -) => AsyncResult; - -/** - * Makes a {@link ReadTextFile} function. - */ -export function makeReadText(debugLog: DebugLog): ReadTextFile { - return (path) => - resultifyFsOp(debugLog, () => fs.readFile(path, { encoding: "utf8" })); -} - -/** - * Function for overwriting the content of a text file. Creates the file - * if it does not exist. - * @param filePath The path to the file. - * @param content The content to write. - */ -export type WriteTextFile = ( - filePath: string, - content: string -) => AsyncResult; - -/** - * Makes a {@link WriteTextFile} function. - */ -export function makeWriteText(debugLog: DebugLog): WriteTextFile { - return (filePath, content) => { - const dirPath = path.dirname(filePath); - return resultifyFsOp(debugLog, () => fse.ensureDir(dirPath)).andThen(() => - resultifyFsOp(debugLog, () => fs.writeFile(filePath, content)) - ); - }; -} - -/** - * Attempts to get the names of all directories in a directory. - * @param directoryPath The directories name. - * @param debugLog Debug-log function. - * TODO: Convert to service function. - */ -export function tryGetDirectoriesIn( - directoryPath: string, - debugLog: DebugLog -): AsyncResult, NodeJS.ErrnoException> { - return resultifyFsOp(debugLog, () => - fs.readdir(directoryPath, { withFileTypes: true }) - ) - .map((entries) => entries.filter((it) => it.isDirectory())) - .map((directories) => directories.map((it) => it.name)); -} diff --git a/src/io/npm-search.ts b/src/io/npm-search.ts index cf52b0da..3db96109 100644 --- a/src/io/npm-search.ts +++ b/src/io/npm-search.ts @@ -1,14 +1,10 @@ -import { AsyncResult, Err, Ok } from "ts-results-es"; import npmFetch from "npm-registry-fetch"; import npmSearch from "libnpmsearch"; import { assertIsHttpError } from "../utils/error-type-guards"; import { UnityPackument } from "../domain/packument"; import { SemanticVersion } from "../domain/semantic-version"; import { Registry } from "../domain/registry"; -import { - GenericNetworkError, - RegistryAuthenticationError, -} from "./common-errors"; +import { RegistryAuthenticationError } from "./common-errors"; import { DebugLog } from "../logging"; /** @@ -21,22 +17,16 @@ export type SearchedPackument = Readonly< } >; -/** - * Error which may occur when searching a npm registry. - */ -export type SearchRegistryError = - | RegistryAuthenticationError - | GenericNetworkError; - /** * Function for searching packuments on a registry. * @param registry The registry to search. * @param keyword The keyword to search. + * @returns The search results. */ export type SearchRegistry = ( registry: Registry, keyword: string -) => AsyncResult, SearchRegistryError>; +) => Promise>; /** * Get npm fetch options. @@ -56,20 +46,15 @@ export const getNpmFetchOptions = function ( * Makes a {@link SearchRegistry} function. */ export function makeSearchRegistry(debugLog: DebugLog): SearchRegistry { - return (registry, keyword) => { - return new AsyncResult( - npmSearch(keyword, getNpmFetchOptions(registry)) - // NOTE: The results of the search will be packument objects, so we can change the type - .then((results) => Ok(results as SearchedPackument[])) - .catch((error) => { - assertIsHttpError(error); - debugLog("A http request failed.", error); - return Err( - error.statusCode === 401 - ? new RegistryAuthenticationError() - : new GenericNetworkError() - ); - }) - ); - }; + return (registry, keyword) => + npmSearch(keyword, getNpmFetchOptions(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); + debugLog("A http request failed.", error); + throw error.statusCode === 401 + ? new RegistryAuthenticationError(registry.url) + : error; + }); } diff --git a/src/io/npmrc-io.ts b/src/io/npmrc-io.ts index a36ea327..c3fe85ac 100644 --- a/src/io/npmrc-io.ts +++ b/src/io/npmrc-io.ts @@ -1,77 +1,53 @@ -import { AsyncResult, Err, Ok, Result } from "ts-results-es"; -import { ReadTextFile, WriteTextFile } from "./fs-result"; +import { ReadTextFile, WriteTextFile } from "./text-file-io"; import { EOL } from "node:os"; import { Npmrc } from "../domain/npmrc"; import path from "path"; -import { RequiredEnvMissingError } from "./upm-config-io"; import { GetHomePath } from "./special-paths"; -import { GenericIOError } from "./common-errors"; - -/** - * Error for when npmrc path could not be determined. - */ -export type FindNpmrcError = RequiredEnvMissingError; /** * Function for determining the path of the users .npmrc file. * @returns The path to the file. */ -export type FindNpmrcPath = () => Result; +export type FindNpmrcPath = () => string; /** * Makes a {@link FindNpmrcPath} function. */ export function makeFindNpmrcPath(getHomePath: GetHomePath): FindNpmrcPath { - return () => getHomePath().map((homePath) => path.join(homePath, ".npmrc")); + return () => { + const homePath = getHomePath(); + return path.join(homePath, ".npmrc"); + }; } -/** - * Error that might occur when loading a npmrc. - */ -export type NpmrcLoadError = GenericIOError; - /** * Function for loading npmrc. * @param path The path to load from. * @returns The npmrc's lines or null if not found. */ -export type LoadNpmrc = ( - path: string -) => AsyncResult; +export type LoadNpmrc = (path: string) => Promise; /** * Makes a {@link LoadNpmrc} function. */ export function makeLoadNpmrc(readFile: ReadTextFile): LoadNpmrc { return (path) => - readFile(path) - .map((content) => content.split(EOL)) - .orElse((error) => - error.code === "ENOENT" ? Ok(null) : Err(new GenericIOError("Read")) - ); + readFile(path, true).then((content) => content?.split(EOL) ?? null); } -/** - * Error that might occur when saving a npmrc. - */ -export type NpmrcSaveError = GenericIOError; - /** * Function for saving npmrc files. Overwrites the content of the file. * @param path The path to the file. * @param npmrc The new lines for the file. */ -export type SaveNpmrc = ( - path: string, - npmrc: Npmrc -) => AsyncResult; +export type SaveNpmrc = (path: string, npmrc: Npmrc) => Promise; /** * Makes a {@link SaveNpmrc} function. */ export function makeSaveNpmrc(writeFile: WriteTextFile): SaveNpmrc { - return (path, npmrc) => { + return async (path, npmrc) => { const content = npmrc.join(EOL); - return writeFile(path, content).mapErr(() => new GenericIOError("Write")); + return await writeFile(path, content); }; } diff --git a/src/io/packument-io.ts b/src/io/packument-io.ts index 004476b2..8dc220dc 100644 --- a/src/io/packument-io.ts +++ b/src/io/packument-io.ts @@ -1,61 +1,49 @@ import RegClient from "another-npm-registry-client"; -import { AsyncResult, Err, Ok } from "ts-results-es"; import { assertIsHttpError } from "../utils/error-type-guards"; import { Registry } from "../domain/registry"; import { DomainName } from "../domain/domain-name"; import { UnityPackument } from "../domain/packument"; -import { - GenericNetworkError, - RegistryAuthenticationError, -} from "./common-errors"; +import { RegistryAuthenticationError } from "./common-errors"; +import { DebugLog } from "../logging"; -/** - * Error which may occur when fetching a packument from a remote registry. - */ -export type FetchPackumentError = - | GenericNetworkError - | RegistryAuthenticationError; /** * Function for fetching a packument from a registry. * @param registry The registry to fetch from. * @param name The name of the packument to fetch. * @returns The packument or null of not found. */ - export type FetchPackument = ( registry: Registry, name: DomainName -) => AsyncResult; +) => Promise; /** * Makes a {@link FetchPackument} function. */ export function makeFetchPackument( - registryClient: RegClient.Instance + registryClient: RegClient.Instance, + debugLog: DebugLog ): FetchPackument { return (registry, name) => { const url = `${registry.url}/${name}`; - return new AsyncResult( - new Promise((resolve) => { - return registryClient.get( - url, - { auth: registry.auth || undefined }, - (error, packument) => { - if (error !== null) { - assertIsHttpError(error); - if (error.statusCode === 404) resolve(Ok(null)); - else - resolve( - Err( - error.statusCode === 401 - ? new RegistryAuthenticationError() - : new GenericNetworkError() - ) - ); - } else resolve(Ok(packument)); - } - ); - }) - ); + return new Promise((resolve, reject) => { + return registryClient.get( + url, + { auth: registry.auth || undefined }, + (error, packument) => { + if (error !== null) { + 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 resolve(packument); + } + ); + }); }; } diff --git a/src/io/project-manifest-io.ts b/src/io/project-manifest-io.ts index 58876d3e..17596c0e 100644 --- a/src/io/project-manifest-io.ts +++ b/src/io/project-manifest-io.ts @@ -3,14 +3,19 @@ import { UnityProjectManifest, } from "../domain/project-manifest"; import path from "path"; -import { AsyncResult, Err, Ok } from "ts-results-es"; -import { ReadTextFile, WriteTextFile } from "./fs-result"; -import { StringFormatError, tryParseJson } from "../utils/string-parsing"; -import { - FileMissingError, - FileParseError, - GenericIOError, -} from "./common-errors"; +import { ReadTextFile, WriteTextFile } from "./text-file-io"; +import { AnyJson } from "@iarna/toml"; +import { CustomError } from "ts-custom-error"; +import { DebugLog } from "../logging"; +import { assertIsError } from "../utils/error-type-guards"; + +export class ManifestMissingError extends CustomError { + public constructor(public expectedPath: string) { + super(); + } +} + +export class ManifestMalformedError extends CustomError {} /** * Determines the path to the package manifest based on the project @@ -21,82 +26,44 @@ export function manifestPathFor(projectPath: string): string { return path.join(projectPath, "Packages/manifest.json"); } -/** - * Error for when the project manifest is missing. - */ -export type ProjectManifestMissingError = FileMissingError<"ProjectManifest">; - -/** - * Makes a new {@link ProjectManifestMissingError}. - * @param filePath The path that was searched. - */ -export function makeProjectManifestMissingError( - filePath: string -): ProjectManifestMissingError { - return new FileMissingError("ProjectManifest", filePath); -} - -/** - * Error for when the project manifest could not be parsed. - */ -export type ProjectManifestParseError = FileParseError<"ProjectManifest">; - -/** - * Makes a {@link ProjectManifestParseError} object. - * @param filePath The path of the file. - */ -export function makeProjectManifestParseError( - filePath: string -): ProjectManifestParseError { - return new FileParseError(filePath, "ProjectManifest"); -} - -/** - * Error which may occur when loading a project manifest. - */ -export type ManifestLoadError = - | ProjectManifestMissingError - | GenericIOError - | StringFormatError<"Json"> - | ProjectManifestParseError; - /** * Function for loading the project manifest for a Unity project. * @param projectPath The path to the project's directory. + * @returns The loaded manifest. */ export type LoadProjectManifest = ( projectPath: string -) => AsyncResult; +) => Promise; /** * Makes a {@link LoadProjectManifest} function. */ export function makeLoadProjectManifest( - readFile: ReadTextFile + readFile: ReadTextFile, + debugLog: DebugLog ): LoadProjectManifest { - return (projectPath) => { + return async (projectPath) => { const manifestPath = manifestPathFor(projectPath); - return readFile(manifestPath) - .mapErr((error) => - error.code === "ENOENT" - ? makeProjectManifestMissingError(manifestPath) - : new GenericIOError("Read") - ) - .andThen(tryParseJson) - .andThen((json) => - typeof json === "object" - ? // TODO: Actually validate the json structure - Ok(json as unknown as UnityProjectManifest) - : Err(makeProjectManifestParseError(manifestPath)) - ); + + const content = await readFile(manifestPath, true); + if (content === null) throw new ManifestMissingError(manifestPath); + + let json: AnyJson; + try { + json = await JSON.parse(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; }; } -/** - * Error which may occur when saving a project manifest. - */ -export type ManifestWriteError = GenericIOError; - /** * Function for replacing the project manifest for a Unity project. * @param projectPath The path to the project's directory. @@ -105,7 +72,7 @@ export type ManifestWriteError = GenericIOError; export type WriteProjectManifest = ( projectPath: string, manifest: UnityProjectManifest -) => AsyncResult; +) => Promise; /** * Makes a {@link WriteProjectManifest} function. @@ -113,13 +80,11 @@ export type WriteProjectManifest = ( export function makeWriteProjectManifest( writeFile: WriteTextFile ): WriteProjectManifest { - return (projectPath, manifest) => { + return async (projectPath, manifest) => { const manifestPath = manifestPathFor(projectPath); manifest = pruneManifest(manifest); const json = JSON.stringify(manifest, null, 2); - return writeFile(manifestPath, json).mapErr( - () => new GenericIOError("Write") - ); + return await writeFile(manifestPath, json); }; } diff --git a/src/io/project-version-io.ts b/src/io/project-version-io.ts index 4ca10f9c..ca5cb484 100644 --- a/src/io/project-version-io.ts +++ b/src/io/project-version-io.ts @@ -1,93 +1,62 @@ import path from "path"; -import { AsyncResult, Err, Ok } from "ts-results-es"; -import { ReadTextFile } from "./fs-result"; -import { StringFormatError, tryParseYaml } from "../utils/string-parsing"; -import { - FileMissingError, - FileParseError, - GenericIOError, -} from "./common-errors"; - -export function projectVersionTxtPathFor(projectDirPath: string) { - return path.join(projectDirPath, "ProjectSettings", "ProjectVersion.txt"); +import { ReadTextFile } from "./text-file-io"; +import * as YAML from "yaml"; +import { CustomError } from "ts-custom-error"; +import { AnyJson } from "@iarna/toml"; +import { assertIsError } from "../utils/error-type-guards"; +import { DebugLog } from "../logging"; + +export class ProjectVersionMissingError extends CustomError { + public constructor(public readonly expectedPath: string) { + super(); + } } -/** - * Error for when the ProjectVersion.txt is missing. - */ -export type ProjectVersionMissingError = FileMissingError<"ProjectVersion.txt">; - -/** - * Makes a {@link ProjectVersionMissingError} object. - * @param filePath The path that was searched. - */ -export function makeProjectVersionMissingError( - filePath: string -): ProjectVersionMissingError { - return new FileMissingError("ProjectVersion.txt", filePath); -} +export class ProjectVersionMalformedError extends CustomError {} -/** - * Error for when the project version could not be parsed. - */ -export type ProjectVersionParseError = FileParseError<"ProjectVersion.txt">; - -/** - * Makes a {@link ProjectVersionParseError} object. - * @param filePath The path of the file. - */ -export function makeProjectVersionParseError( - filePath: string -): ProjectVersionParseError { - return new FileParseError(filePath, "ProjectVersion.txt"); +export function projectVersionTxtPathFor(projectDirPath: string) { + return path.join(projectDirPath, "ProjectSettings", "ProjectVersion.txt"); } -/** - * Error which may occur when loading a project-version. - */ -export type ProjectVersionLoadError = - | ProjectVersionMissingError - | GenericIOError - | StringFormatError<"Yaml"> - | ProjectVersionParseError; - /** * Function for loading a projects editor version string. * @param projectDirPath The path to the projects root directory. * @returns A string describing the projects editor version ie 2020.2.1f1. */ -export type LoadProjectVersion = ( - projectDirPath: string -) => AsyncResult; +export type LoadProjectVersion = (projectDirPath: string) => Promise; /** * Makes a {@link LoadProjectVersion} function. */ export function makeLoadProjectVersion( - readFile: ReadTextFile + readFile: ReadTextFile, + debugLog: DebugLog ): LoadProjectVersion { - return (projectDirPath) => { + return async (projectDirPath) => { const filePath = projectVersionTxtPathFor(projectDirPath); - return readFile(filePath) - .mapErr((error) => - error.code === "ENOENT" - ? makeProjectVersionMissingError(filePath) - : new GenericIOError("Read") + const content = await readFile(filePath, true); + if (content === null) throw new ProjectVersionMissingError(filePath); + + let yaml: AnyJson; + try { + yaml = YAML.parse(content); + } catch (error) { + assertIsError(error); + debugLog("ProjectVersion.txt has malformed yaml.", error); + throw new ProjectVersionMalformedError(); + } + + if ( + !( + typeof yaml === "object" && + yaml !== null && + "m_EditorVersion" in yaml && + typeof yaml.m_EditorVersion === "string" ) - .andThen(tryParseYaml) - .andThen((content) => { - if ( - !( - typeof content === "object" && - content !== null && - "m_EditorVersion" in content && - typeof content.m_EditorVersion === "string" - ) - ) - return Err(makeProjectVersionParseError(filePath)); + ) + throw new ProjectVersionMalformedError(); - return Ok(content.m_EditorVersion); - }); + return yaml.m_EditorVersion; }; } diff --git a/src/io/special-paths.ts b/src/io/special-paths.ts index 6f912d01..d1b52dba 100644 --- a/src/io/special-paths.ts +++ b/src/io/special-paths.ts @@ -1,5 +1,4 @@ import { Err, Ok, Result } from "ts-results-es"; -import { RequiredEnvMissingError } from "./upm-config-io"; import os from "os"; import { CustomError } from "ts-custom-error"; import { @@ -16,7 +15,6 @@ import { tryGetEnv } from "../utils/env-util"; * Error for when a specific OS does not support a specific editor-version. */ export class VersionNotSupportedOnOsError extends CustomError { - private readonly _class = "VersionNotSupportedOnOsError"; constructor( /** * The version that is not supported. @@ -35,7 +33,6 @@ export class VersionNotSupportedOnOsError extends CustomError { * Error for when Unity does not support an OS. */ export class OSNotSupportedError extends CustomError { - private readonly _class = "OSNotSupportedError"; constructor( /** * Name of the unsupported OS. @@ -46,16 +43,13 @@ export class OSNotSupportedError extends CustomError { } } -/** - * Error which may occur when getting the users home path. - */ -export type GetHomePathError = RequiredEnvMissingError; - /** * Function for getting the path of the users home directory. * @returns The path to the directory. */ -export type GetHomePath = () => Result; +export type GetHomePath = () => string; + +export class NoHomePathError extends CustomError {} /** * Makes a {@link GetHomePath} function. @@ -63,9 +57,8 @@ export type GetHomePath = () => Result; export function makeGetHomePath(): GetHomePath { return () => { const homePath = tryGetEnv("USERPROFILE") ?? tryGetEnv("HOME"); - if (homePath === null) - return Err(new RequiredEnvMissingError(["USERPROFILE", "HOME"])); - return Ok(homePath); + if (homePath === null) throw new NoHomePathError(); + return homePath; }; } diff --git a/src/io/text-file-io.ts b/src/io/text-file-io.ts new file mode 100644 index 00000000..58d4a656 --- /dev/null +++ b/src/io/text-file-io.ts @@ -0,0 +1,56 @@ +import fs from "fs/promises"; +import { assertIsNodeError } from "../utils/error-type-guards"; +import fse from "fs-extra"; +import path from "path"; + +/** + * Function for loading the content of a text file. + */ +export type ReadTextFile = { + /** + * @param path The path to the file. + * @param optional The file is expected to exist. Throws if not found. + * @returns The files text content. + */ + (path: string, optional: false): Promise; + /** + * @param path The path to the file. + * @param optional The file is expected to potentially not exist. + * @returns The files text content or null if file was missing. + */ + (path: string, optional: true): Promise; +}; + +/** + * Makes a {@link ReadTextFile} function. + */ +export function makeReadText(): ReadTextFile { + return ((path, optional) => + fs.readFile(path, { encoding: "utf8" }).catch((error) => { + assertIsNodeError(error); + if (optional && error.code === "ENOENT") return null; + throw error; + })) as ReadTextFile; +} + +/** + * Function for overwriting the content of a text file. Creates the file + * if it does not exist. + * @param filePath The path to the file. + * @param content The content to write. + */ +export type WriteTextFile = ( + filePath: string, + content: string +) => Promise; + +/** + * Makes a {@link WriteTextFile} function. + */ +export function makeWriteText(): WriteTextFile { + return async (filePath, content) => { + const dirPath = path.dirname(filePath); + await fse.ensureDir(dirPath); + await fs.writeFile(filePath, content); + }; +} diff --git a/src/io/upm-config-io.ts b/src/io/upm-config-io.ts index 00dd70fc..07054b6c 100644 --- a/src/io/upm-config-io.ts +++ b/src/io/upm-config-io.ts @@ -1,36 +1,16 @@ import path from "path"; import TOML from "@iarna/toml"; import { UPMConfig } from "../domain/upm-config"; -import { CustomError } from "ts-custom-error"; -import { AsyncResult, Err, Ok } from "ts-results-es"; -import { ReadTextFile, WriteTextFile } from "./fs-result"; +import { ReadTextFile, WriteTextFile } from "./text-file-io"; import { tryGetEnv } from "../utils/env-util"; -import { StringFormatError, tryParseToml } from "../utils/string-parsing"; -import { tryGetWslPath, WslPathError } from "./wsl"; -import { ChildProcessError, RunChildProcess } from "./child-process"; +import { tryGetWslPath } from "./wsl"; +import { RunChildProcess } from "./child-process"; import { GetHomePath } from "./special-paths"; -import { GenericIOError } from "./common-errors"; +import { CustomError } from "ts-custom-error"; const configFileName = ".upmconfig.toml"; -export class RequiredEnvMissingError extends CustomError { - private readonly _class = "RequiredEnvMissingError"; - constructor(public readonly keyNames: string[]) { - super( - `Env was required to contain a value for one of the following keys, but all were missing: ${keyNames - .map((keyName) => `"${keyName}"`) - .join(", ")}.` - ); - } -} - -/** - * Error which may occur when getting the upmconfig file path. - */ -export type GetUpmConfigPathError = - | WslPathError - | RequiredEnvMissingError - | ChildProcessError; +export class NoSystemUserProfilePath extends CustomError {} /** * Function which gets the path to the upmconfig file. @@ -41,7 +21,7 @@ export type GetUpmConfigPathError = export type GetUpmConfigPath = ( wsl: boolean, systemUser: boolean -) => AsyncResult; +) => Promise; /** * Makes a {@link GetUpmConfigPath} function. @@ -50,41 +30,32 @@ export function makeGetUpmConfigPath( getHomePath: GetHomePath, runChildProcess: RunChildProcess ): GetUpmConfigPath { - function getConfigDirectory(wsl: boolean, systemUser: boolean) { + async function getConfigDirectory(wsl: boolean, systemUser: boolean) { const systemUserSubPath = "Unity/config/ServiceAccounts"; if (wsl) { if (systemUser) - return tryGetWslPath("ALLUSERSPROFILE", runChildProcess).map((it) => - path.join(it, systemUserSubPath) + return await tryGetWslPath("ALLUSERSPROFILE", runChildProcess).then( + (it) => path.join(it, systemUserSubPath) ); - return tryGetWslPath("USERPROFILE", runChildProcess); + return await tryGetWslPath("USERPROFILE", runChildProcess); } if (systemUser) { const profilePath = tryGetEnv("ALLUSERSPROFILE"); - if (profilePath === null) - return Err( - new RequiredEnvMissingError(["ALLUSERSPROFILE"]) - ).toAsyncResult(); - return Ok(path.join(profilePath, systemUserSubPath)).toAsyncResult(); + if (profilePath === null) throw new NoSystemUserProfilePath(); + return path.join(profilePath, systemUserSubPath); } - return getHomePath().toAsyncResult(); + return getHomePath(); } - return (wsl, systemUser) => { - return getConfigDirectory(wsl, systemUser).map((directory) => - path.join(directory, configFileName) - ); + return async (wsl, systemUser) => { + const directory = await getConfigDirectory(wsl, systemUser); + return path.join(directory, configFileName); }; } -/** - * Error which may occur when loading a {@link UPMConfig}. - */ -export type UpmConfigLoadError = GenericIOError | StringFormatError<"Toml">; - /** * IO function for loading an upm-config file. * @param configFilePath Path of the upm-config file. @@ -92,31 +63,20 @@ export type UpmConfigLoadError = GenericIOError | StringFormatError<"Toml">; */ export type LoadUpmConfig = ( configFilePath: string -) => AsyncResult; +) => Promise; /** * Makes a {@link LoadUpmConfig} function. */ export function makeLoadUpmConfig(readFile: ReadTextFile): LoadUpmConfig { - return (configFilePath) => - readFile(configFilePath) - .andThen(tryParseToml) - // TODO: Actually validate - .map((toml) => toml as UPMConfig | null) - .orElse((error) => - !(error instanceof StringFormatError) - ? error.code === "ENOENT" - ? Ok(null) - : Err(new GenericIOError("Read")) - : Err(error) - ); + return async (configFilePath) => { + const content = await readFile(configFilePath, true); + if (content === null) return null; + const toml = TOML.parse(content); + return toml as UPMConfig; + }; } -/** - * Errors which may occur when saving a UPM-config file. - */ -export type UpmConfigSaveError = GenericIOError; - /** * Save the upm config. * @param config The config to save. @@ -125,7 +85,7 @@ export type UpmConfigSaveError = GenericIOError; export type SaveUpmConfig = ( config: UPMConfig, configFilePath: string -) => AsyncResult; +) => Promise; /** * Creates a {@link SaveUpmConfig} function. @@ -133,8 +93,6 @@ export type SaveUpmConfig = ( export function makeSaveUpmConfig(writeFile: WriteTextFile): SaveUpmConfig { return (config, configFilePath) => { const content = TOML.stringify(config); - return writeFile(configFilePath, content).mapErr( - () => new GenericIOError("Write") - ); + return writeFile(configFilePath, content); }; } diff --git a/src/io/wsl.ts b/src/io/wsl.ts index e6951b14..530a2b91 100644 --- a/src/io/wsl.ts +++ b/src/io/wsl.ts @@ -1,23 +1,16 @@ -import { AsyncResult, Err } from "ts-results-es"; import { CustomError } from "ts-custom-error"; import isWsl from "is-wsl"; -import { ChildProcessError, RunChildProcess } from "./child-process"; +import { RunChildProcess } from "./child-process"; /** * Error for when attempting to interact with wsl on a non-wsl system. */ export class NoWslError extends CustomError { - private readonly _class = "NoWslError"; constructor() { - super("No WSL detected."); + super(); } } -/** - * Error which may occur when resolving a wsl path. - */ -export type WslPathError = NoWslError | ChildProcessError; - /** * Attempt to resolve the wls path for a variable. * @param varName The variable name. @@ -25,10 +18,10 @@ export type WslPathError = NoWslError | ChildProcessError; export function tryGetWslPath( varName: string, runChildProcess: RunChildProcess -): AsyncResult { - if (!isWsl) return Err(new NoWslError()).toAsyncResult(); +): Promise { + if (!isWsl) throw new NoWslError(); - return runChildProcess(`wslpath "$(wslvar ${varName})"`).map((output) => + return runChildProcess(`wslpath "$(wslvar ${varName})"`).then((output) => output.trim() ); } diff --git a/src/packument-version-resolving.ts b/src/packument-version-resolving.ts index 4dc34fdd..89262345 100644 --- a/src/packument-version-resolving.ts +++ b/src/packument-version-resolving.ts @@ -1,7 +1,6 @@ import { VersionReference } from "./domain/package-reference"; import { DomainName } from "./domain/domain-name"; import { - NoVersionsError, tryResolvePackumentVersion, UnityPackument, UnityPackumentVersion, @@ -12,7 +11,6 @@ import { PackumentCache, tryGetFromCache } from "./packument-cache"; import { RegistryUrl } from "./domain/registry-url"; import { Err, Result } from "ts-results-es"; import { PackumentNotFoundError } from "./common-errors"; -import { ResolveRemotePackumentVersionError } from "./services/resolve-remote-packument-version"; /** * A version-reference that is resolvable. @@ -43,9 +41,8 @@ export interface ResolvedPackumentVersion { /** * A failed attempt at resolving a packument-version. */ -export type PackumentVersionResolveError = +export type ResolvePackumentVersionError = | PackumentNotFoundError - | NoVersionsError | VersionNotFoundError; /** @@ -60,7 +57,7 @@ export function tryResolveFromCache( source: RegistryUrl, packumentName: DomainName, requestedVersion: ResolvableVersion -): Result { +): Result { const cachedPackument = tryGetFromCache(cache, source, packumentName); if (cachedPackument === null) return Err(new PackumentNotFoundError(packumentName)); @@ -81,9 +78,9 @@ export function tryResolveFromCache( * @returns The more fixable failure. */ export function pickMostFixable( - a: Err, - b: Err -): Err { + a: Err, + b: Err +): Err { // Anything is more fixable than packument-not-found if ( a.error instanceof PackumentNotFoundError && diff --git a/src/services/built-in-package-check.ts b/src/services/built-in-package-check.ts index 7bb8f59d..04dc28ae 100644 --- a/src/services/built-in-package-check.ts +++ b/src/services/built-in-package-check.ts @@ -1,26 +1,9 @@ import { DomainName } from "../domain/domain-name"; import { SemanticVersion } from "../domain/semantic-version"; -import { AsyncResult } from "ts-results-es"; -import { - CheckIsUnityPackage, - CheckIsUnityPackageError, -} from "./unity-package-check"; +import { CheckIsUnityPackage } from "./unity-package-check"; import { FetchPackument } from "../io/packument-io"; import { unityRegistryUrl } from "../domain/registry-url"; import { recordKeys } from "../utils/record-utils"; -import { AsyncOk } from "../utils/result-utils"; -import { - GenericNetworkError, - RegistryAuthenticationError, -} from "../io/common-errors"; -import { FetchAllPackumentsError } from "../io/all-packuments-io"; - -/** - * Error which may occur when checking whether a package is built-in. - */ -export type CheckIsBuiltInPackageError = - | CheckIsUnityPackageError - | FetchAllPackumentsError; /** * Function for checking whether a specific package version is built-in. @@ -31,7 +14,7 @@ export type CheckIsBuiltInPackageError = export type CheckIsBuiltInPackage = ( packageName: DomainName, version: SemanticVersion -) => AsyncResult; +) => Promise; /** * Makes a {@link CheckIsBuiltInPackage} function. @@ -40,32 +23,22 @@ export function makeCheckIsBuiltInPackage( checkIsUnityPackage: CheckIsUnityPackage, fetchPackument: FetchPackument ): CheckIsBuiltInPackage { - function checkExistsOnUnityRegistry( + async function checkExistsOnUnityRegistry( packageName: DomainName, version: SemanticVersion - ): AsyncResult { - return fetchPackument({ url: unityRegistryUrl, auth: null }, packageName) - .map((maybePackument) => { - if (maybePackument === null) return false; - const versions = recordKeys(maybePackument.versions); - return versions.includes(version); - }) - .mapErr((error) => { - if (error instanceof RegistryAuthenticationError) - throw new Error( - "Authentication with Unity registry failed, even though it does not require authentication." - ); - - return error; - }); + ): Promise { + const packument = await fetchPackument( + { url: unityRegistryUrl, auth: null }, + packageName + ); + if (packument === null) return false; + const versions = recordKeys(packument.versions); + return versions.includes(version); } - return (packageName, version) => { - return checkIsUnityPackage(packageName).andThen((isUnityPackage) => { - if (!isUnityPackage) return AsyncOk(false); - return checkExistsOnUnityRegistry(packageName, version).map( - (existsOnUnityRegistry) => !existsOnUnityRegistry - ); - }); + return async (packageName, version) => { + const isUnityPackage = await checkIsUnityPackage(packageName); + if (!isUnityPackage) return false; + return !(await checkExistsOnUnityRegistry(packageName, version)); }; } diff --git a/src/services/dependency-resolving.ts b/src/services/dependency-resolving.ts index 14f484c7..8cf4b469 100644 --- a/src/services/dependency-resolving.ts +++ b/src/services/dependency-resolving.ts @@ -2,35 +2,21 @@ import { DomainName } from "../domain/domain-name"; import { SemanticVersion } from "../domain/semantic-version"; import { addToCache, emptyPackumentCache } from "../packument-cache"; import { - PackumentVersionResolveError, pickMostFixable, ResolvableVersion, ResolvedPackumentVersion, + ResolvePackumentVersionError, tryResolveFromCache, } from "../packument-version-resolving"; import { RegistryUrl } from "../domain/registry-url"; import { Registry } from "../domain/registry"; -import { - ResolveRemotePackumentVersion, - ResolveRemotePackumentVersionError, -} from "./resolve-remote-packument-version"; +import { ResolveRemotePackumentVersion } from "./resolve-remote-packument-version"; import { areArraysEqual } from "../utils/array-utils"; import { dependenciesOf } from "../domain/package-manifest"; -import { - ResolveLatestVersion, - ResolveLatestVersionError, -} from "./resolve-latest-version"; -import { Err, Ok, Result } from "ts-results-es"; +import { ResolveLatestVersion } from "./resolve-latest-version"; +import { Err, Result } from "ts-results-es"; import { PackumentNotFoundError } from "../common-errors"; -import { FetchPackumentError } from "../io/packument-io"; -import { - GenericNetworkError, - RegistryAuthenticationError, -} from "../io/common-errors"; -import { - CheckIsBuiltInPackage, - CheckIsBuiltInPackageError, -} from "./built-in-package-check"; +import { CheckIsBuiltInPackage } from "./built-in-package-check"; export type DependencyBase = { /** @@ -63,19 +49,11 @@ export interface ValidDependency extends DependencyBase { * A dependency that could not be resolved. */ export interface InvalidDependency extends DependencyBase { - reason: PackumentVersionResolveError; + reason: ResolvePackumentVersionError; } type NameVersionPair = Readonly<[DomainName, SemanticVersion]>; -/** - * Error which may occur when resolving the dependencies for a package. - */ -export type DependencyResolveError = - | ResolveLatestVersionError - | FetchPackumentError - | CheckIsBuiltInPackageError; - /** * Function for resolving all dependencies for a package. * @param sources Sources from which dependencies can be resolved. @@ -88,9 +66,7 @@ export type ResolveDependencies = ( name: DomainName, version: SemanticVersion | "latest" | undefined, deep: boolean -) => Promise< - Result<[ValidDependency[], InvalidDependency[]], DependencyResolveError> ->; +) => Promise<[ValidDependency[], InvalidDependency[]]>; /** * Makes a {@link ResolveDependencies} function. @@ -103,14 +79,14 @@ export function makeResolveDependency( // TODO: Add tests for this service return async (sources, name, version, deep) => { - const latestVersionResult = + const latestVersion = version === undefined || version === "latest" - ? await resolveLatestVersion(sources, name).map((it) => it.value) - .promise - : Ok(version); - if (latestVersionResult.isErr()) return latestVersionResult; + ? await resolveLatestVersion(sources, name).then( + (it) => it?.value || null + ) + : version; - const latestVersion = latestVersionResult.value; + if (latestVersion == null) throw new PackumentNotFoundError(name); // a list of pending dependency {name, version} const pendingList = Array.of([name, latestVersion]); @@ -156,12 +132,7 @@ export function makeResolveDependency( if (!isProcessed) { // add entry to processed list processedList.push(entry); - const isInternalResult = await checkIsBuiltInPackage( - entryName, - entryVersion - ).promise; - if (isInternalResult.isErr()) return isInternalResult; - const isInternal = isInternalResult.value; + const isInternal = await checkIsBuiltInPackage(entryName, entryVersion); const isSelf = entryName === name; if (isInternal) { @@ -177,7 +148,7 @@ export function makeResolveDependency( // Search all given registries. let resolveResult: Result< ResolvedPackumentVersion, - ResolveRemotePackumentVersionError + ResolvePackumentVersionError > = Err(new PackumentNotFoundError(entryName)); for (const source of sources) { const result = await tryResolveFromRegistry( @@ -195,17 +166,10 @@ export function makeResolveDependency( } if (resolveResult.isErr()) { - const error = resolveResult.error; - if ( - error instanceof GenericNetworkError || - error instanceof RegistryAuthenticationError - ) - return Err(error); - depsInvalid.push({ name: entryName, self: isSelf, - reason: error, + reason: resolveResult.error, }); continue; } @@ -234,6 +198,6 @@ export function makeResolveDependency( depsValid.push(dependency); } } - return Ok([depsValid, depsInvalid]); + return [depsValid, depsInvalid]; }; } diff --git a/src/services/determine-editor-version.ts b/src/services/determine-editor-version.ts index 568142e3..88b013fd 100644 --- a/src/services/determine-editor-version.ts +++ b/src/services/determine-editor-version.ts @@ -1,18 +1,9 @@ -import { AsyncResult } from "ts-results-es"; import { isRelease, ReleaseVersion, tryParseEditorVersion, } from "../domain/editor-version"; -import { - LoadProjectVersion, - ProjectVersionLoadError, -} from "../io/project-version-io"; - -/** - * Error which may occur when determining the editor-version. - */ -export type DetermineEditorVersionError = ProjectVersionLoadError; +import { LoadProjectVersion } from "../io/project-version-io"; /** * Function for determining the editor-version for a Unity project. @@ -22,7 +13,7 @@ export type DetermineEditorVersionError = ProjectVersionLoadError; */ export type DetermineEditorVersion = ( projectPath: string -) => AsyncResult; +) => Promise; /** * Makes a {@link DetermineEditorVersion} function. @@ -30,12 +21,11 @@ export type DetermineEditorVersion = ( export function makeDetermineEditorVersion( loadProjectVersion: LoadProjectVersion ): DetermineEditorVersion { - return (projectPath) => { - return loadProjectVersion(projectPath).map((unparsedEditorVersion) => { - const parsedEditorVersion = tryParseEditorVersion(unparsedEditorVersion); - return parsedEditorVersion !== null && isRelease(parsedEditorVersion) - ? parsedEditorVersion - : unparsedEditorVersion; - }); + return async (projectPath) => { + const unparsedEditorVersion = await loadProjectVersion(projectPath); + const parsedEditorVersion = tryParseEditorVersion(unparsedEditorVersion); + return parsedEditorVersion !== null && isRelease(parsedEditorVersion) + ? parsedEditorVersion + : unparsedEditorVersion; }; } diff --git a/src/services/login.ts b/src/services/login.ts index 030ea6c4..6c71b094 100644 --- a/src/services/login.ts +++ b/src/services/login.ts @@ -1,19 +1,10 @@ -import { AsyncResult } from "ts-results-es"; import { BasicAuth, encodeBasicAuth, TokenAuth } from "../domain/upm-config"; import { RegistryUrl } from "../domain/registry-url"; -import { SaveAuthToUpmConfig, UpmAuthStoreError } from "./upm-auth"; -import { NpmLogin, NpmLoginError } from "./npm-login"; -import { AuthNpmrc, NpmrcAuthTokenUpdateError } from "./npmrc-auth"; +import { SaveAuthToUpmConfig } from "./upm-auth"; +import { NpmLogin } from "./npm-login"; +import { AuthNpmrc } from "./npmrc-auth"; import { DebugLog } from "../logging"; -/** - * Error which may occur when logging in a user. - */ -export type LoginError = - | UpmAuthStoreError - | NpmLoginError - | NpmrcAuthTokenUpdateError; - /** * Function for logging in a user to a npm registry. Supports both basic and * token-based authentication. @@ -39,7 +30,7 @@ export type Login = ( registry: RegistryUrl, configPath: string, authMode: "basic" | "token" -) => AsyncResult; +) => Promise; /** * Makes a {@link Login} function. @@ -50,7 +41,7 @@ export function makeLogin( authNpmrc: AuthNpmrc, debugLog: DebugLog ): Login { - return ( + return async ( username, password, email, @@ -62,7 +53,7 @@ export function makeLogin( if (authMode === "basic") { // basic auth const _auth = encodeBasicAuth(username, password); - return saveAuthToUpmConfig(configPath, registry, { + return await saveAuthToUpmConfig(configPath, registry, { email, alwaysAuth, _auth, @@ -70,18 +61,15 @@ export function makeLogin( } // npm login - return npmLogin(registry, username, password, email).andThen((token) => { - debugLog(`npm login successful`); - // write npm token - return authNpmrc(registry, token).andThen((npmrcPath) => { - debugLog(`saved to npm config: ${npmrcPath}`); - // Save config - return saveAuthToUpmConfig(configPath, registry, { - email, - alwaysAuth, - token, - } satisfies TokenAuth); - }); - }); + const token = await npmLogin(registry, username, password, email); + debugLog(`npm login successful`); + + const npmrcPath = await authNpmrc(registry, token); + await saveAuthToUpmConfig(configPath, registry, { + email, + alwaysAuth, + token, + } satisfies TokenAuth); + debugLog(`saved to npm config: ${npmrcPath}`); }; } diff --git a/src/services/npm-login.ts b/src/services/npm-login.ts index 9f1618d5..6247923e 100644 --- a/src/services/npm-login.ts +++ b/src/services/npm-login.ts @@ -1,17 +1,8 @@ import RegClient from "another-npm-registry-client"; import { RegistryUrl } from "../domain/registry-url"; -import { AsyncResult, Err, Ok } from "ts-results-es"; -import { - GenericNetworkError, - RegistryAuthenticationError, -} from "../io/common-errors"; +import { RegistryAuthenticationError } from "../io/common-errors"; import { DebugLog } from "../logging"; -/** - * Error which may occur when logging a user into a npm registry. - */ -export type NpmLoginError = GenericNetworkError | RegistryAuthenticationError; - /** * A token authenticating a user. */ @@ -30,7 +21,7 @@ export type NpmLogin = ( username: string, email: string, password: string -) => AsyncResult; +) => Promise; /** * Makes a new {@link NpmLogin} function. @@ -39,28 +30,21 @@ export function makeNpmLogin( registryClient: RegClient.Instance, debugLog: DebugLog ): NpmLogin { - return (registryUrl, username, email, password) => { - return new AsyncResult( - new Promise((resolve) => { - registryClient.adduser( - registryUrl, - { auth: { username, email, password } }, - (error, responseData, _, response) => { - if (response !== undefined && !responseData.ok) { - debugLog("A http request failed.", response); - resolve( - Err( - response.statusCode === 401 - ? new RegistryAuthenticationError() - : new GenericNetworkError() - ) - ); - } else if (responseData.ok) resolve(Ok(responseData.token)); - - // TODO: Handle error - } - ); - }) - ); - }; + return (registryUrl, username, email, password) => + new Promise((resolve, reject) => { + registryClient.adduser( + registryUrl, + { auth: { username, email, password } }, + (error, responseData, _, response) => { + if (response !== undefined && !responseData.ok) { + debugLog( + "Npm registry login failed because of not-ok response.", + response + ); + reject(new RegistryAuthenticationError(registryUrl)); + } else if (responseData.ok) resolve(responseData.token); + reject(error); + } + ); + }); } diff --git a/src/services/npmrc-auth.ts b/src/services/npmrc-auth.ts index f55c3d82..9567c2bf 100644 --- a/src/services/npmrc-auth.ts +++ b/src/services/npmrc-auth.ts @@ -1,22 +1,6 @@ import { RegistryUrl } from "../domain/registry-url"; -import { AsyncResult } from "ts-results-es"; -import { - FindNpmrcPath, - LoadNpmrc, - NpmrcLoadError, - NpmrcSaveError, - SaveNpmrc, -} from "../io/npmrc-io"; +import { FindNpmrcPath, LoadNpmrc, SaveNpmrc } from "../io/npmrc-io"; import { emptyNpmrc, setToken } from "../domain/npmrc"; -import { RequiredEnvMissingError } from "../io/upm-config-io"; - -/** - * Error that might occur when updating an auth-token inside a npmrc file. - */ -export type NpmrcAuthTokenUpdateError = - | RequiredEnvMissingError - | NpmrcLoadError - | NpmrcSaveError; /** * Function for updating the user-wide npm-auth token inside a users @@ -28,23 +12,18 @@ export type NpmrcAuthTokenUpdateError = export type AuthNpmrc = ( registry: RegistryUrl, token: string -) => AsyncResult; +) => Promise; export function makeAuthNpmrc( findPath: FindNpmrcPath, loadNpmrc: LoadNpmrc, saveNpmrc: SaveNpmrc ): AuthNpmrc { - return (registry, token) => { - // read config - return findPath() - .toAsyncResult() - .andThen((configPath) => - loadNpmrc(configPath) - .map((maybeNpmrc) => maybeNpmrc ?? emptyNpmrc) - .map((npmrc) => setToken(npmrc, registry, token)) - .andThen((npmrc) => saveNpmrc(configPath, npmrc)) - .map(() => configPath) - ); + return async (registry, token) => { + const configPath = findPath(); + const initial = (await loadNpmrc(configPath)) || emptyNpmrc; + const updated = setToken(initial, registry, token); + await saveNpmrc(configPath, updated); + return configPath; }; } diff --git a/src/services/parse-env.ts b/src/services/parse-env.ts index d105cd1c..412b589a 100644 --- a/src/services/parse-env.ts +++ b/src/services/parse-env.ts @@ -1,19 +1,23 @@ import chalk from "chalk"; -import { - GetUpmConfigPath, - GetUpmConfigPathError, - LoadUpmConfig, - UpmConfigLoadError, -} from "../io/upm-config-io"; +import { GetUpmConfigPath, LoadUpmConfig } from "../io/upm-config-io"; import path from "path"; import { coerceRegistryUrl, makeRegistryUrl } from "../domain/registry-url"; import { tryGetAuthForRegistry, UPMConfig } from "../domain/upm-config"; import { CmdOptions } from "../cli/options"; -import { Ok, Result } from "ts-results-es"; import { tryGetEnv } from "../utils/env-util"; import { Registry } from "../domain/registry"; import { Logger } from "npmlog"; import { GetCwd } from "../io/special-paths"; +import { CustomError } from "ts-custom-error"; +import { DebugLog } from "../logging"; +import { assertIsError } from "../utils/error-type-guards"; + +/** + * Error for when auth information for a registry could not be loaded. + */ +export class RegistryAuthLoadError extends CustomError { + // noinspection JSUnusedLocalSymbols +} export type Env = Readonly<{ cwd: string; @@ -24,15 +28,13 @@ export type Env = Readonly<{ registry: Registry; }>; -export type EnvParseError = GetUpmConfigPathError | UpmConfigLoadError; - /** * Function for parsing environment information and global * command-options for further usage. + * @param options The options passed to the current command. + * @returns Environment information. */ -export type ParseEnv = ( - options: CmdOptions -) => Promise>; +export type ParseEnv = (options: CmdOptions) => Promise; /** * Creates a {@link ParseEnv} function. @@ -41,7 +43,8 @@ export function makeParseEnv( log: Logger, getUpmConfigPath: GetUpmConfigPath, loadUpmConfig: LoadUpmConfig, - getCwd: GetCwd + getCwd: GetCwd, + debugLog: DebugLog ): ParseEnv { function determineCwd(options: CmdOptions): string { return options._global.chdir !== undefined @@ -76,7 +79,7 @@ export function makeParseEnv( return { url, auth }; } - function determineUpstreamRegistry(options: CmdOptions): Registry { + function determineUpstreamRegistry(): Registry { const url = makeRegistryUrl("https://packages.unity.com"); return { url, auth: null }; @@ -117,25 +120,30 @@ export function makeParseEnv( const wsl = determineWsl(options); // registries - const upmConfigResult = await getUpmConfigPath(wsl, systemUser).andThen( - loadUpmConfig - ).promise; - if (upmConfigResult.isErr()) return upmConfigResult; - const upmConfig = upmConfigResult.value; - - const registry = determinePrimaryRegistry(options, upmConfig); - const upstreamRegistry = determineUpstreamRegistry(options); + const upmConfigPath = await getUpmConfigPath(wsl, systemUser); + + let registry: Registry; + let upstreamRegistry: Registry; + try { + const upmConfig = await loadUpmConfig(upmConfigPath); + registry = determinePrimaryRegistry(options, upmConfig); + upstreamRegistry = determineUpstreamRegistry(); + } catch (error) { + assertIsError(error); + debugLog("Upmconfig load or parsing failed.", error); + throw new RegistryAuthLoadError(); + } // cwd const cwd = determineCwd(options); - return Ok({ + return { cwd, registry, systemUser, upstream, upstreamRegistry, wsl, - }); + }; }; } diff --git a/src/services/remove-packages.ts b/src/services/remove-packages.ts index 1af14a18..46ca2aa2 100644 --- a/src/services/remove-packages.ts +++ b/src/services/remove-packages.ts @@ -7,22 +7,18 @@ import { PackumentNotFoundError } from "../common-errors"; import { DomainName } from "../domain/domain-name"; import { LoadProjectManifest, - ManifestLoadError, - ManifestWriteError, WriteProjectManifest, } from "../io/project-manifest-io"; import { SemanticVersion } from "../domain/semantic-version"; import { PackageUrl } from "../domain/package-url"; +import { resultifyAsyncOp } from "../utils/result-utils"; export type RemovedPackage = { name: DomainName; version: SemanticVersion | PackageUrl; }; -export type RemovePackagesError = - | ManifestLoadError - | PackumentNotFoundError - | ManifestWriteError; +export type RemovePackagesError = PackumentNotFoundError; export type RemovePackages = ( projectPath: string, @@ -39,9 +35,8 @@ export function makeRemovePackages( ): Result<[UnityProjectManifest, RemovedPackage], PackumentNotFoundError> { // not found array const versionInManifest = manifest.dependencies[packageName]; - if (versionInManifest === undefined) { + if (versionInManifest === undefined) return Err(new PackumentNotFoundError(packageName)); - } manifest = removeDependency(manifest, packageName); @@ -85,17 +80,19 @@ export function makeRemovePackages( return (projectPath, packageNames) => { // load manifest - const initialManifest = loadProjectManifest(projectPath); + const initialManifest = resultifyAsyncOp< + UnityProjectManifest, + RemovePackagesError + >(loadProjectManifest(projectPath)); // remove const removeResult = initialManifest.andThen((it) => tryRemoveAll(it, packageNames) ); - return removeResult.andThen(([updatedManifest, removedPackages]) => - writeProjectManifest(projectPath, updatedManifest).map( - () => removedPackages - ) - ); + return removeResult.map(async ([updatedManifest, removedPackages]) => { + await writeProjectManifest(projectPath, updatedManifest); + return removedPackages; + }); }; } diff --git a/src/services/resolve-latest-version.ts b/src/services/resolve-latest-version.ts index f248d726..e44e4e9b 100644 --- a/src/services/resolve-latest-version.ts +++ b/src/services/resolve-latest-version.ts @@ -1,54 +1,41 @@ import { Registry } from "../domain/registry"; import { DomainName } from "../domain/domain-name"; -import { AsyncResult, Err, Ok } from "ts-results-es"; import { SemanticVersion } from "../domain/semantic-version"; -import { PackumentNotFoundError } from "../common-errors"; -import { - NoVersionsError, - tryResolvePackumentVersion, -} from "../domain/packument"; -import { FetchPackument, FetchPackumentError } from "../io/packument-io"; +import { tryResolvePackumentVersion } from "../domain/packument"; +import { FetchPackument } from "../io/packument-io"; import { FromRegistry, queryAllRegistriesLazy } from "../utils/sources"; -/** - * Error which may occur when resolving the latest version for a package. - */ -export type ResolveLatestVersionError = - | PackumentNotFoundError - | FetchPackumentError - | NoVersionsError; - /** * Service for resolving the latest published version of a package. * @param sources All sources to check for the package. * @param packageName The name of the package to search. + * @returns The resolved version or null if the package does not exist on the + * registry. */ export type ResolveLatestVersion = ( sources: ReadonlyArray, packageName: DomainName -) => AsyncResult, ResolveLatestVersionError>; +) => Promise | null>; export function makeResolveLatestVersion( fetchPackument: FetchPackument ): ResolveLatestVersion { - function tryResolveFrom( + async function tryResolveFrom( source: Registry, packageName: DomainName - ): AsyncResult { - return fetchPackument(source, packageName).andThen((maybePackument) => { - if (maybePackument === null) return Ok(null); - return tryResolvePackumentVersion(maybePackument, "latest").map( - (it) => it.version - ); - }); + ): Promise { + const packument = await fetchPackument(source, packageName); + if (packument === null) return null; + + const resolveResult = tryResolvePackumentVersion(packument, "latest"); + if (resolveResult.isErr()) throw resolveResult.error; + + return resolveResult.value.version; } - return (sources, packageName) => - queryAllRegistriesLazy(sources, (source) => + return (sources, packageName) => { + return queryAllRegistriesLazy(sources, (source) => tryResolveFrom(source, packageName) - ).andThen((resolved) => - resolved !== null - ? Ok(resolved) - : Err(new PackumentNotFoundError(packageName)) ); + }; } diff --git a/src/services/resolve-remote-packument-version.ts b/src/services/resolve-remote-packument-version.ts index ba5f1523..bd598c36 100644 --- a/src/services/resolve-remote-packument-version.ts +++ b/src/services/resolve-remote-packument-version.ts @@ -1,21 +1,18 @@ import { DomainName } from "../domain/domain-name"; import { Registry } from "../domain/registry"; -import { AsyncResult, Err, Ok } from "ts-results-es"; +import { AsyncResult, Err } from "ts-results-es"; import { - PackumentVersionResolveError, ResolvableVersion, ResolvedPackumentVersion, + ResolvePackumentVersionError, } from "../packument-version-resolving"; import { PackumentNotFoundError } from "../common-errors"; -import { tryResolvePackumentVersion } from "../domain/packument"; -import { FetchPackument, FetchPackumentError } from "../io/packument-io"; - -/** - * Error which may occur when resolving a remove packument version. - */ -export type ResolveRemotePackumentVersionError = - | PackumentVersionResolveError - | FetchPackumentError; +import { + tryResolvePackumentVersion, + UnityPackument, +} from "../domain/packument"; +import { FetchPackument } from "../io/packument-io"; +import { resultifyAsyncOp } from "../utils/result-utils"; /** * Function for resolving remove packument versions. @@ -27,7 +24,7 @@ export type ResolveRemotePackumentVersion = ( packageName: DomainName, requestedVersion: ResolvableVersion, source: Registry -) => AsyncResult; +) => AsyncResult; /** * Makes a {@link ResolveRemotePackumentVersion} function. @@ -36,19 +33,17 @@ export function makeResolveRemotePackumentVersion( fetchPackument: FetchPackument ): ResolveRemotePackumentVersion { return (packageName, requestedVersion, source) => - fetchPackument(source, packageName) - .andThen((maybePackument) => { - if (maybePackument === null) - return Err(new PackumentNotFoundError(packageName)); - return Ok(maybePackument); - }) - .andThen((packument) => - tryResolvePackumentVersion(packument, requestedVersion).map( - (packumentVersion) => ({ - packument, - packumentVersion, - source: source.url, - }) - ) + resultifyAsyncOp( + fetchPackument(source, packageName) + ).andThen((packument) => { + if (packument === null) + return Err(new PackumentNotFoundError(packageName)); + return tryResolvePackumentVersion(packument, requestedVersion).map( + (packumentVersion) => ({ + packument, + packumentVersion, + source: source.url, + }) ); + }); } diff --git a/src/services/search-packages.ts b/src/services/search-packages.ts index eb4232ea..7a122458 100644 --- a/src/services/search-packages.ts +++ b/src/services/search-packages.ts @@ -1,21 +1,8 @@ import { Registry } from "../domain/registry"; -import { AsyncResult } from "ts-results-es"; import { SearchedPackument, SearchRegistry } from "../io/npm-search"; -import { - FetchAllPackuments, - FetchAllPackumentsError, -} from "../io/all-packuments-io"; -import { - GenericNetworkError, - RegistryAuthenticationError, -} from "../io/common-errors"; - -/** - * Error which may occur when searching for packages. - */ -export type SearchPackagesError = - | RegistryAuthenticationError - | GenericNetworkError; +import { FetchAllPackuments } from "../io/all-packuments-io"; +import { DebugLog } from "../logging"; +import { assertIsError } from "../utils/error-type-guards"; /** * A function for searching packages in a registry. @@ -28,39 +15,40 @@ export type SearchPackages = ( registry: Registry, keyword: string, onUseAllFallback?: () => void -) => AsyncResult, SearchPackagesError>; +) => Promise>; /** * Makes a {@licence SearchPackages} function. */ export function makeSearchPackages( searchRegistry: SearchRegistry, - fetchAllPackuments: FetchAllPackuments + fetchAllPackuments: FetchAllPackuments, + debugLog: DebugLog ): SearchPackages { - function searchInAll( + async function searchInAll( registry: Registry, keyword: string - ): AsyncResult { - return fetchAllPackuments(registry).map((allPackuments) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { _updated, ...packumentEntries } = allPackuments; - const packuments = Object.values(packumentEntries); - - // filter keyword - const klc = keyword.toLowerCase(); - - return packuments.filter((packument) => - packument.name.toLowerCase().includes(klc) - ); - }); + ): Promise { + const allPackuments = await fetchAllPackuments(registry); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { _updated, ...packumentEntries } = allPackuments; + const packuments = Object.values(packumentEntries); + const klc = keyword.toLowerCase(); + return packuments.filter((packument) => + packument.name.toLowerCase().includes(klc) + ); } - return (registry, keyword, onUseOldSearch) => { - // search endpoint - return searchRegistry(registry, keyword).orElse(() => { + return async (registry, keyword, onUseOldSearch) => { + try { + // search endpoint + return await searchRegistry(registry, keyword); + } catch (error) { + assertIsError(error); + debugLog("Searching using search endpoint failed", error); // search old search onUseOldSearch && onUseOldSearch(); - return searchInAll(registry, keyword); - }); + return await searchInAll(registry, keyword); + } }; } diff --git a/src/services/unity-package-check.ts b/src/services/unity-package-check.ts index ec69996b..9e481abb 100644 --- a/src/services/unity-package-check.ts +++ b/src/services/unity-package-check.ts @@ -1,21 +1,12 @@ import { DomainName } from "../domain/domain-name"; -import { AsyncResult } from "ts-results-es"; import { CheckUrlExists } from "../io/check-url"; -import { GenericNetworkError } from "../io/common-errors"; - -/** - * Error which may occur when checking whether a package is a Unity package. - */ -export type CheckIsUnityPackageError = GenericNetworkError; /** * 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 -) => AsyncResult; +export type CheckIsUnityPackage = (packageName: DomainName) => Promise; /** * Makes a {@link CheckIsUnityPackage} function. diff --git a/src/services/upm-auth.ts b/src/services/upm-auth.ts index 090148b6..4ce790c3 100644 --- a/src/services/upm-auth.ts +++ b/src/services/upm-auth.ts @@ -1,17 +1,6 @@ import { RegistryUrl } from "../domain/registry-url"; import { addAuth, UpmAuth } from "../domain/upm-config"; -import { AsyncResult } from "ts-results-es"; -import { - LoadUpmConfig, - SaveUpmConfig, - UpmConfigLoadError, -} from "../io/upm-config-io"; -import { GenericIOError } from "../io/common-errors"; - -/** - * Errors which may occur when storing an {@link UpmAuth} to the file-system. - */ -export type UpmAuthStoreError = UpmConfigLoadError | GenericIOError; +import { LoadUpmConfig, SaveUpmConfig } from "../io/upm-config-io"; /** * Function for storing authentication information in an upmconfig file. @@ -23,8 +12,7 @@ export type SaveAuthToUpmConfig = ( configPath: string, registry: RegistryUrl, auth: UpmAuth -) => AsyncResult; - +) => Promise; /** * Makes a {@link SaveAuthToUpmConfig} function. */ @@ -33,9 +21,9 @@ export function makeSaveAuthToUpmConfig( saveUpmConfig: SaveUpmConfig ): SaveAuthToUpmConfig { // TODO: Add tests for this service - return (configPath, registry, auth) => - loadUpmConfig(configPath) - .map((maybeConfig) => maybeConfig || {}) - .map((config) => addAuth(registry, auth, config)) - .andThen((config) => saveUpmConfig(config, configPath)); + return async (configPath, registry, auth) => { + const initialConfig = (await loadUpmConfig(configPath)) || {}; + const updatedConfig = addAuth(registry, auth, initialConfig); + await saveUpmConfig(updatedConfig, configPath); + }; } diff --git a/src/utils/error-type-guards.ts b/src/utils/error-type-guards.ts index cff16202..51120e28 100644 --- a/src/utils/error-type-guards.ts +++ b/src/utils/error-type-guards.ts @@ -1,14 +1,23 @@ -import assert, { AssertionError } from "assert"; +import { AssertionError } from "assert"; import { HttpErrorBase } from "npm-registry-fetch/lib/errors"; +export function isError(x: unknown): x is Error { + return x !== null && typeof x === "object"; +} /** * @throws {AssertionError} The given parameter is not an error. */ export function assertIsError(x: unknown): asserts x is Error { - assert(x !== null); - assert(typeof x === "object"); - assert("name" in x); - assert("message" in x); + if (!isError(x)) + throw new AssertionError({ + message: "Value is not an error.", + actual: x, + expected: {}, + }); +} + +export function isNodeError(x: unknown): x is NodeJS.ErrnoException { + return isError(x) && "code" in x && "errno" in x; } /** @@ -20,19 +29,28 @@ export function assertIsError(x: unknown): asserts x is Error { export function assertIsNodeError( x: unknown ): asserts x is NodeJS.ErrnoException { - assertIsError(x); - assert("code" in x); + if (!isNodeError(x)) + throw new AssertionError({ + message: "Value is not a node error.", + actual: x, + expected: { + code: "string", + errno: "string", + }, + }); } -export function assertIsHttpError(x: unknown): asserts x is HttpErrorBase { - assertIsError(x); - if (!("statusCode" in x)) - throw new AssertionError({ message: "Argument was not an HTTP-error." }); +export function isHttpError(x: unknown): x is HttpErrorBase { + return isError(x) && "statusCode" in x; } -export const isHttpError = (x: unknown): x is HttpErrorBase => { - return x instanceof Error && "statusCode" in x; -}; - -export const is404Error = (err: HttpErrorBase): boolean => - err.statusCode === 404 || err.message.includes("404"); +export function assertIsHttpError(x: unknown): asserts x is HttpErrorBase { + if (!isHttpError(x)) + throw new AssertionError({ + message: "Value is not an http error.", + actual: x, + expected: { + statusCode: "number", + }, + }); +} diff --git a/src/utils/result-utils.ts b/src/utils/result-utils.ts index 0808bd9e..cc8cce2d 100644 --- a/src/utils/result-utils.ts +++ b/src/utils/result-utils.ts @@ -1,4 +1,5 @@ -import { AsyncResult, Err, Ok } from "ts-results-es"; +import { AsyncResult, Err, Ok, Result } from "ts-results-es"; +import { isPromise } from "node:util/types"; /** * Creates an ok {@link AsyncResult} with a given value. @@ -23,3 +24,15 @@ export function AsyncOk(value?: unknown) { export function AsyncErr(error: T): AsyncResult { return Err(error).toAsyncResult(); } + +/** + * Wraps a promise or operation returning a promise into an {@link AsyncResult}. + * @param op The operation to wrap. + */ +export function resultifyAsyncOp( + op: (() => Promise) | Promise +): AsyncResult { + return new AsyncResult( + Result.wrapAsync(isPromise(op) ? () => op : op) + ); +} diff --git a/src/utils/sources.ts b/src/utils/sources.ts index e2aadaec..f8e2241a 100644 --- a/src/utils/sources.ts +++ b/src/utils/sources.ts @@ -1,7 +1,5 @@ import { Registry } from "../domain/registry"; -import { AsyncResult } from "ts-results-es"; import { RegistryUrl } from "../domain/registry-url"; -import { AsyncOk } from "./result-utils"; /** * A value that was resolved from a remote registry. @@ -25,29 +23,28 @@ export type FromRegistry = { * @returns The first resolved value or null if no registry matched the query. * Can also return the first error that caused a query to fail. */ -export function queryAllRegistriesLazy( +export function queryAllRegistriesLazy( sources: ReadonlyArray, - query: (source: Registry) => AsyncResult -): AsyncResult | null, TError> { - function queryRecursively( + query: (source: Registry) => Promise +): Promise | null> { + async function queryRecursively( remainingSources: ReadonlyArray - ): AsyncResult | null, TError> { + ): Promise | null> { // If there are no more sources to search then can return with null - if (remainingSources.length === 0) return AsyncOk(null); + if (remainingSources.length === 0) return null; // Determine current and fallback sources const currentSource = remainingSources[0]!; const fallbackSources = remainingSources.slice(1); // Query the current source first - return query(currentSource).andThen((maybeValue) => - // Afterward check if we got a value. - // If yes we can return it, otherwise we enter the next level - // of the recursion with the remaining registries. - maybeValue !== null - ? AsyncOk({ value: maybeValue, source: currentSource.url }) - : queryRecursively(fallbackSources) - ); + const maybeValue = await query(currentSource); + // Afterward check if we got a value. + // If yes we can return it, otherwise we enter the next level + // of the recursion with the remaining registries. + return maybeValue !== null + ? { value: maybeValue, source: currentSource.url } + : await queryRecursively(fallbackSources); } return queryRecursively(sources); diff --git a/src/utils/string-parsing.ts b/src/utils/string-parsing.ts deleted file mode 100644 index 67e08a2a..00000000 --- a/src/utils/string-parsing.ts +++ /dev/null @@ -1,57 +0,0 @@ -import TOML, { AnyJson, JsonMap } from "@iarna/toml"; -import { Result } from "ts-results-es"; -import { CustomError } from "ts-custom-error"; -import yaml from "yaml"; -import { assertIsError } from "./error-type-guards"; - -/** - * Error for when a string could not be parsed to a specific format. - */ -export class StringFormatError< - const TFormat extends string -> extends CustomError { - // noinspection JSUnusedLocalSymbols - private readonly _class = "StringFormatError"; - - public constructor( - /** - * Description of the format the string was supposed to be parsed to. - */ - public readonly formatName: TFormat, - /** - * The error which caused the parsing failure. - */ - public readonly cause: Error - ) { - super(); - } -} - -function makeParser( - formatName: TFormat, - parsingFunction: (x: string) => T -): (x: string) => Result> { - return (input) => - Result.wrap(() => parsingFunction(input)).mapErr((error) => { - assertIsError(error); - return new StringFormatError(formatName, error); - }); -} - -/** - * Attempts to parse a json-string. - * @param json The string to be parsed. - */ -export const tryParseJson = makeParser<"Json", AnyJson>("Json", JSON.parse); - -/** - * Attempts to parse a toml-string. - * @param toml The string to be parsed. - */ -export const tryParseToml = makeParser<"Toml", JsonMap>("Toml", TOML.parse); - -/** - * Attempts to parse a yaml-string. - * @param input The string to be parsed. - */ -export const tryParseYaml = makeParser<"Yaml", AnyJson>("Yaml", yaml.parse); diff --git a/test/cli/cmd-add.test.ts b/test/cli/cmd-add.test.ts index f8c02851..70c3705d 100644 --- a/test/cli/cmd-add.test.ts +++ b/test/cli/cmd-add.test.ts @@ -1,13 +1,14 @@ -import { makeAddCmd } from "../../src/cli/cmd-add"; +import { + CompatibilityCheckFailedError, + makeAddCmd, + PackageIncompatibleError, + UnresolvedDependenciesError, +} from "../../src/cli/cmd-add"; import { makeDomainName } from "../../src/domain/domain-name"; import { Env, ParseEnv } from "../../src/services/parse-env"; import { exampleRegistryUrl } from "../domain/data-registry"; import { unityRegistryUrl } from "../../src/domain/registry-url"; import { makeEditorVersion } from "../../src/domain/editor-version"; -import { - mockProjectManifest, - mockProjectManifestWriteResult, -} from "../io/project-manifest-io.mock"; import { emptyProjectManifest } from "../../src/domain/project-manifest"; import { makeMockLogger } from "./log.mock"; import { buildPackument } from "../domain/data-packument"; @@ -21,14 +22,16 @@ import { LoadProjectManifest, WriteProjectManifest, } from "../../src/io/project-manifest-io"; -import { makePackageReference } from "../../src/domain/package-reference"; -import { VersionNotFoundError } from "../../src/domain/packument"; +import { + UnityPackumentVersion, + VersionNotFoundError, +} from "../../src/domain/packument"; import { noopLogger } from "../../src/logging"; -import { GenericIOError } from "../../src/io/common-errors"; import { DetermineEditorVersion } from "../../src/services/determine-editor-version"; -import { Err, Ok } from "ts-results-es"; import { ResultCodes } from "../../src/cli/result-codes"; -import { AsyncOk } from "../../src/utils/result-utils"; +import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; +import { PackumentNotFoundError } from "../../src/common-errors"; +import { ResolvedPackumentVersion } from "../../src/packument-version-resolving"; const somePackage = makeDomainName("com.some.package"); const otherPackage = makeDomainName("com.other.package"); @@ -60,7 +63,7 @@ const defaultEnv = { function makeDependencies() { const parseEnv = mockService(); - parseEnv.mockResolvedValue(Ok(defaultEnv)); + parseEnv.mockResolvedValue(defaultEnv); const resolveRemovePackumentVersion = mockService(); @@ -71,35 +74,33 @@ function makeDependencies() { ); const resolveDependencies = mockService(); - resolveDependencies.mockResolvedValue( - Ok([ - [ - { - name: somePackage, - version: makeSemanticVersion("1.0.0"), - source: exampleRegistryUrl, - self: true, - }, - { - name: otherPackage, - version: makeSemanticVersion("1.0.0"), - source: exampleRegistryUrl, - self: false, - }, - ], - [], - ]) - ); + resolveDependencies.mockResolvedValue([ + [ + { + name: somePackage, + version: makeSemanticVersion("1.0.0"), + source: exampleRegistryUrl, + self: true, + }, + { + name: otherPackage, + version: makeSemanticVersion("1.0.0"), + source: exampleRegistryUrl, + self: false, + }, + ], + [], + ]); const loadProjectManifest = mockService(); - mockProjectManifest(loadProjectManifest, emptyProjectManifest); + loadProjectManifest.mockResolvedValue(emptyProjectManifest); const writeProjectManifest = mockService(); - mockProjectManifestWriteResult(writeProjectManifest); + writeProjectManifest.mockResolvedValue(undefined); const determineEditorVersion = mockService(); - determineEditorVersion.mockReturnValue( - AsyncOk(makeEditorVersion(2022, 2, 1, "f", 2)) + determineEditorVersion.mockResolvedValue( + makeEditorVersion(2022, 2, 1, "f", 2) ); const log = makeMockLogger(); @@ -127,124 +128,9 @@ function makeDependencies() { } describe("cmd-add", () => { - it("should fail if env could not be parsed", async () => { - const expected = new GenericIOError("Read"); - const { addCmd, parseEnv } = makeDependencies(); - parseEnv.mockResolvedValue(Err(expected)); - - const resultCode = await addCmd(somePackage, { _global: {} }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - - it("should notify if env could not be parsed", async () => { - const { addCmd, parseEnv, log } = makeDependencies(); - parseEnv.mockResolvedValue(Err(new GenericIOError("Read"))); - - await addCmd(somePackage, { _global: {} }); - - expect(log.error).toHaveBeenCalledWith( - expect.any(String), - expect.stringContaining("file-system error") - ); - }); - - it("should fail if editor-version could not be determined", async () => { - const { addCmd, determineEditorVersion } = makeDependencies(); - determineEditorVersion.mockReturnValue( - Err(new GenericIOError("Read")).toAsyncResult() - ); - - const resultCode = await addCmd(somePackage, { _global: {} }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - - it("should notify if editor-version could not be determined", async () => { - const { addCmd, determineEditorVersion, log } = makeDependencies(); - determineEditorVersion.mockReturnValue( - Err(new GenericIOError("Read")).toAsyncResult() - ); - - await addCmd(somePackage, { _global: {} }); - - expect(log.error).toHaveBeenCalledWith( - expect.any(String), - expect.stringContaining("file-system error") - ); - }); - - it("should fail if manifest could not be loaded", async () => { - const { addCmd, loadProjectManifest } = makeDependencies(); - mockProjectManifest(loadProjectManifest, null); - - const resultCode = await addCmd(somePackage, { _global: {} }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - - it("should notify if manifest could not be loaded", async () => { - const { addCmd, loadProjectManifest, log } = makeDependencies(); - mockProjectManifest(loadProjectManifest, null); - - await addCmd(somePackage, { _global: {} }); - - expect(log.error).toHaveBeenCalledWith( - expect.any(String), - expect.stringContaining("Could not locate") - ); - }); - - it("should fail if package could not be resolved", async () => { - const { addCmd, resolveRemovePackumentVersion } = makeDependencies(); - mockResolvedPackuments(resolveRemovePackumentVersion); - - const resultCode = await addCmd(somePackage, { _global: {} }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - - it("should notify if package could not be resolved", async () => { - const { addCmd, resolveRemovePackumentVersion, log } = makeDependencies(); - mockResolvedPackuments(resolveRemovePackumentVersion); - - await addCmd(somePackage, { _global: {} }); - - expect(log.error).toHaveBeenCalledWith( - expect.any(String), - expect.stringContaining("not found") - ); - }); - - it("should fail if package version could not be resolved", async () => { - const { addCmd } = makeDependencies(); - - const resultCode = await addCmd( - makePackageReference(somePackage, "2.0.0"), - { - _global: {}, - } - ); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - - it("should notify if package version could not be resolved", async () => { - const { addCmd, log } = makeDependencies(); - - await addCmd(makePackageReference(somePackage, "2.0.0"), { - _global: {}, - }); - - expect(log.error).toHaveBeenCalledWith( - expect.any(String), - expect.stringContaining("has no published version") - ); - }); - it("should notify if editor-version is unknown", async () => { const { addCmd, determineEditorVersion, log } = makeDependencies(); - determineEditorVersion.mockReturnValue(AsyncOk("bad version")); + determineEditorVersion.mockResolvedValue("bad version"); await addCmd(somePackage, { _global: {}, @@ -256,54 +142,6 @@ describe("cmd-add", () => { ); }); - it("should notify if package editor version is not valid", async () => { - const { addCmd, resolveRemovePackumentVersion, log } = makeDependencies(); - mockResolvedPackuments(resolveRemovePackumentVersion, [ - exampleRegistryUrl, - badEditorPackument, - ]); - - await addCmd(somePackage, { - _global: {}, - }); - - expect(log.warn).toHaveBeenCalledWith( - "package.unity", - expect.stringContaining("not valid") - ); - }); - - it("should suggest running with force if package editor version is not valid", async () => { - const { addCmd, resolveRemovePackumentVersion, log } = makeDependencies(); - mockResolvedPackuments(resolveRemovePackumentVersion, [ - exampleRegistryUrl, - badEditorPackument, - ]); - - await addCmd(somePackage, { - _global: {}, - }); - - expect(log.notice).toHaveBeenCalledWith( - "suggest", - expect.stringContaining("run with option -f") - ); - }); - - it("should fail if package editor version is not valid and not running with force", async () => { - const { addCmd, resolveRemovePackumentVersion } = makeDependencies(); - mockResolvedPackuments(resolveRemovePackumentVersion, [ - exampleRegistryUrl, - badEditorPackument, - ]); - - const resultCode = await addCmd(somePackage, { - _global: {}, - }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - it("should add package with invalid editor version when running with force", async () => { const { addCmd, resolveRemovePackumentVersion } = makeDependencies(); mockResolvedPackuments(resolveRemovePackumentVersion, [ @@ -319,41 +157,7 @@ describe("cmd-add", () => { expect(resultCode).toEqual(ResultCodes.Ok); }); - it("should notify if package is incompatible with editor", async () => { - const { addCmd, resolveRemovePackumentVersion, log } = makeDependencies(); - mockResolvedPackuments(resolveRemovePackumentVersion, [ - exampleRegistryUrl, - incompatiblePackument, - ]); - - await addCmd(somePackage, { - _global: {}, - }); - - expect(log.warn).toHaveBeenCalledWith( - "editor.version", - expect.stringContaining("requires") - ); - }); - - it("should suggest to run with force if package is incompatible with editor", async () => { - const { addCmd, resolveRemovePackumentVersion, log } = makeDependencies(); - mockResolvedPackuments(resolveRemovePackumentVersion, [ - exampleRegistryUrl, - incompatiblePackument, - ]); - - await addCmd(somePackage, { - _global: {}, - }); - - expect(log.notice).toHaveBeenCalledWith( - "suggest", - expect.stringContaining("run with option -f") - ); - }); - - it("should fail if package is incompatible with editor and not running with force", async () => { + it("should add package with incompatible with editor when running with force", async () => { const { addCmd, resolveRemovePackumentVersion } = makeDependencies(); mockResolvedPackuments(resolveRemovePackumentVersion, [ exampleRegistryUrl, @@ -362,49 +166,24 @@ describe("cmd-add", () => { const resultCode = await addCmd(somePackage, { _global: {}, + force: true, }); - expect(resultCode).toEqual(ResultCodes.Error); + expect(resultCode).toEqual(ResultCodes.Ok); }); - it("should add package with incompatible with editor when running with force", async () => { + it("should fail when adding package with incompatible with editor and not running with force", async () => { const { addCmd, resolveRemovePackumentVersion } = makeDependencies(); mockResolvedPackuments(resolveRemovePackumentVersion, [ exampleRegistryUrl, incompatiblePackument, ]); - const resultCode = await addCmd(somePackage, { - _global: {}, - force: true, - }); - - expect(resultCode).toEqual(ResultCodes.Ok); - }); - - it("should notify of unresolved dependencies", async () => { - const { addCmd, resolveDependencies, log } = makeDependencies(); - resolveDependencies.mockResolvedValue( - Ok([ - [], - [ - { - name: otherPackage, - self: false, - reason: new VersionNotFoundError(makeSemanticVersion("1.0.0"), []), - }, - ], - ]) - ); - - await addCmd(somePackage, { - _global: {}, - }); - - expect(log.error).toHaveBeenCalledWith( - expect.any(String), - expect.stringContaining("has no published version") - ); + await expect( + addCmd(somePackage, { + _global: {}, + }) + ).rejects.toBeInstanceOf(PackageIncompatibleError); }); it("should not fetch dependencies for upstream packages", async () => { @@ -422,92 +201,78 @@ describe("cmd-add", () => { expect(resolveDependencies).not.toHaveBeenCalled(); }); - it("should suggest to install missing dependency version manually", async () => { - const { addCmd, resolveDependencies, log } = makeDependencies(); - resolveDependencies.mockResolvedValue( - Ok([ - [], - [ - { - name: otherPackage, - self: false, - reason: new VersionNotFoundError(makeSemanticVersion("1.0.0"), []), - }, - ], - ]) + it("should fail if package could not be resolved", async () => { + const { addCmd, resolveRemovePackumentVersion } = makeDependencies(); + resolveRemovePackumentVersion.mockReturnValue( + AsyncErr(new PackumentNotFoundError(somePackage)) ); - await addCmd(somePackage, { - _global: {}, - }); - - expect(log.notice).toHaveBeenCalledWith( - "suggest", - expect.stringContaining("manually") - ); + await expect(() => + addCmd(somePackage, { + _global: {}, + }) + ).rejects.toBeInstanceOf(PackumentNotFoundError); }); - it("should suggest to run with force if dependency could not be resolved", async () => { - const { addCmd, resolveDependencies, log } = makeDependencies(); - resolveDependencies.mockResolvedValue( - Ok([ - [], - [ - { - name: otherPackage, - self: false, - reason: new VersionNotFoundError(makeSemanticVersion("1.0.0"), []), - }, - ], - ]) + it("should fail if packument had malformed target editor and not running with force", async () => { + const { addCmd, resolveRemovePackumentVersion } = makeDependencies(); + resolveRemovePackumentVersion.mockReturnValue( + AsyncOk({ + packumentVersion: { + name: somePackage, + version: makeSemanticVersion("1.0.0"), + unity: "bad vesion", + } as unknown as UnityPackumentVersion, + } as unknown as ResolvedPackumentVersion) ); - await addCmd(somePackage, { - _global: {}, - }); - - expect(log.error).toHaveBeenCalledWith( - "missing dependencies", - expect.stringContaining("run with option -f") - ); + await expect(() => + addCmd(somePackage, { + _global: {}, + }) + ).rejects.toBeInstanceOf(CompatibilityCheckFailedError); }); it("should fail if dependency could not be resolved and not running with force", async () => { const { addCmd, resolveDependencies } = makeDependencies(); - resolveDependencies.mockResolvedValue( - Ok([ - [], - [ - { - name: otherPackage, - self: false, - reason: new VersionNotFoundError(makeSemanticVersion("1.0.0"), []), - }, - ], - ]) - ); - - const resultCode = await addCmd(somePackage, { - _global: {}, - }); + resolveDependencies.mockResolvedValue([ + [], + [ + { + name: otherPackage, + self: false, + reason: new VersionNotFoundError( + otherPackage, + makeSemanticVersion("1.0.0"), + [] + ), + }, + ], + ]); - expect(resultCode).toEqual(ResultCodes.Error); + await expect(() => + addCmd(somePackage, { + _global: {}, + }) + ).rejects.toBeInstanceOf(UnresolvedDependenciesError); }); it("should add package with unresolved dependency when running with force", async () => { const { addCmd, resolveDependencies } = makeDependencies(); - resolveDependencies.mockResolvedValue( - Ok([ - [], - [ - { - name: otherPackage, - self: false, - reason: new VersionNotFoundError(makeSemanticVersion("1.0.0"), []), - }, - ], - ]) - ); + resolveDependencies.mockResolvedValue([ + [], + [ + { + name: otherPackage, + self: false, + reason: new VersionNotFoundError( + otherPackage, + makeSemanticVersion("1.0.0"), + [] + ), + }, + ], + ]); const resultCode = await addCmd(somePackage, { _global: {}, @@ -548,8 +313,7 @@ describe("cmd-add", () => { it("should replace package", async () => { const { addCmd, writeProjectManifest, loadProjectManifest } = makeDependencies(); - mockProjectManifest( - loadProjectManifest, + loadProjectManifest.mockResolvedValue( buildProjectManifest((manifest) => manifest.addDependency(somePackage, "0.1.0", true, true) ) @@ -569,8 +333,7 @@ describe("cmd-add", () => { it("should notify if package was replaced", async () => { const { addCmd, loadProjectManifest, log } = makeDependencies(); - mockProjectManifest( - loadProjectManifest, + loadProjectManifest.mockResolvedValue( buildProjectManifest((manifest) => manifest.addDependency(somePackage, "0.1.0", true, true) ) @@ -588,8 +351,7 @@ describe("cmd-add", () => { it("should notify if package is already in manifest", async () => { const { addCmd, loadProjectManifest, log } = makeDependencies(); - mockProjectManifest( - loadProjectManifest, + loadProjectManifest.mockResolvedValue( buildProjectManifest((manifest) => manifest.addDependency(somePackage, "1.0.0", true, true) ) @@ -645,8 +407,7 @@ describe("cmd-add", () => { it("should not save if nothing changed", async () => { const { addCmd, loadProjectManifest, writeProjectManifest } = makeDependencies(); - mockProjectManifest( - loadProjectManifest, + loadProjectManifest.mockResolvedValue( buildProjectManifest((manifest) => manifest .addDependency(somePackage, "1.0.0", true, false) @@ -667,7 +428,7 @@ describe("cmd-add", () => { // The second package can not be added await addCmd([somePackage, makeDomainName("com.unknown.package")], { _global: {}, - }); + }).catch(() => {}); // Because adding is atomic the manifest should only be written if // all packages were added. @@ -686,29 +447,4 @@ describe("cmd-add", () => { expect.stringContaining("open Unity") ); }); - - it("should fail if manifest could not be saved", async () => { - const expected = new GenericIOError("Write"); - const { addCmd, writeProjectManifest } = makeDependencies(); - mockProjectManifestWriteResult(writeProjectManifest, expected); - - const resultCode = await addCmd(somePackage, { _global: {} }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - - it("should notify if manifest could not be saved", async () => { - const { addCmd, writeProjectManifest, log } = makeDependencies(); - mockProjectManifestWriteResult( - writeProjectManifest, - new GenericIOError("Write") - ); - - await addCmd(somePackage, { _global: {} }); - - expect(log.error).toHaveBeenCalledWith( - expect.any(String), - expect.stringContaining("file-system error") - ); - }); }); diff --git a/test/cli/cmd-deps.test.ts b/test/cli/cmd-deps.test.ts index a6bc3206..ce338094 100644 --- a/test/cli/cmd-deps.test.ts +++ b/test/cli/cmd-deps.test.ts @@ -1,6 +1,5 @@ import { makeDepsCmd } from "../../src/cli/cmd-deps"; import { Env, ParseEnv } from "../../src/services/parse-env"; -import { Err, Ok } from "ts-results-es"; import { exampleRegistryUrl } from "../domain/data-registry"; import { unityRegistryUrl } from "../../src/domain/registry-url"; import { makeDomainName } from "../../src/domain/domain-name"; @@ -12,7 +11,6 @@ import { ResolveDependencies } from "../../src/services/dependency-resolving"; import { mockService } from "../services/service.mock"; import { VersionNotFoundError } from "../../src/domain/packument"; import { noopLogger } from "../../src/logging"; -import { GenericIOError } from "../../src/io/common-errors"; import { ResultCodes } from "../../src/cli/result-codes"; const somePackage = makeDomainName("com.some.package"); @@ -25,28 +23,26 @@ const defaultEnv = { function makeDependencies() { const parseEnv = mockService(); - parseEnv.mockResolvedValue(Ok(defaultEnv)); + parseEnv.mockResolvedValue(defaultEnv); const resolveDependencies = mockService(); - resolveDependencies.mockResolvedValue( - Ok([ - [ - { - source: exampleRegistryUrl, - self: true, - name: somePackage, - version: makeSemanticVersion("1.2.3"), - }, - { - source: exampleRegistryUrl, - self: false, - name: otherPackage, - version: makeSemanticVersion("1.2.3"), - }, - ], - [], - ]) - ); + resolveDependencies.mockResolvedValue([ + [ + { + source: exampleRegistryUrl, + self: true, + name: somePackage, + version: makeSemanticVersion("1.2.3"), + }, + { + source: exampleRegistryUrl, + self: false, + name: otherPackage, + version: makeSemanticVersion("1.2.3"), + }, + ], + [], + ]); const log = makeMockLogger(); @@ -55,16 +51,6 @@ function makeDependencies() { } describe("cmd-deps", () => { - it("should fail if env could not be parsed", async () => { - const expected = new GenericIOError("Read"); - const { depsCmd, parseEnv } = makeDependencies(); - parseEnv.mockResolvedValue(Err(expected)); - - const resultCode = await depsCmd(somePackage, { _global: {} }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - it("should fail if package-reference has url-version", async () => { const { depsCmd } = makeDependencies(); @@ -109,18 +95,16 @@ describe("cmd-deps", () => { it("should log missing dependency", async () => { const { depsCmd, resolveDependencies, log } = makeDependencies(); - resolveDependencies.mockResolvedValue( - Ok([ - [], - [ - { - name: otherPackage, - self: false, - reason: new PackumentNotFoundError(otherPackage), - }, - ], - ]) - ); + resolveDependencies.mockResolvedValue([ + [], + [ + { + name: otherPackage, + self: false, + reason: new PackumentNotFoundError(otherPackage), + }, + ], + ]); await depsCmd(somePackage, { _global: {}, @@ -134,18 +118,20 @@ describe("cmd-deps", () => { it("should log missing dependency version", async () => { const { depsCmd, resolveDependencies, log } = makeDependencies(); - resolveDependencies.mockResolvedValue( - Ok([ - [], - [ - { - name: otherPackage, - self: false, - reason: new VersionNotFoundError(makeSemanticVersion("1.2.3"), []), - }, - ], - ]) - ); + resolveDependencies.mockResolvedValue([ + [], + [ + { + name: otherPackage, + self: false, + reason: new VersionNotFoundError( + otherPackage, + makeSemanticVersion("1.2.3"), + [] + ), + }, + ], + ]); await depsCmd(somePackage, { _global: {}, diff --git a/test/cli/cmd-login.test.ts b/test/cli/cmd-login.test.ts index 5d1061f6..71e8421d 100644 --- a/test/cli/cmd-login.test.ts +++ b/test/cli/cmd-login.test.ts @@ -1,21 +1,11 @@ -import { Err, Ok } from "ts-results-es"; import { makeLoginCmd } from "../../src/cli/cmd-login"; import { mockService } from "../services/service.mock"; import { Env, ParseEnv } from "../../src/services/parse-env"; -import { - GetUpmConfigPath, - RequiredEnvMissingError, -} from "../../src/io/upm-config-io"; +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"; -import { - GenericIOError, - RegistryAuthenticationError, -} from "../../src/io/common-errors"; -import { ResultCodes } from "../../src/cli/result-codes"; -import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; const defaultEnv = { cwd: "/users/some-user/projects/SomeProject", @@ -31,13 +21,13 @@ const exampleUpmConfigPath = "/user/home/.upmconfig.toml"; describe("cmd-login", () => { function makeDependencies() { const parseEnv = mockService(); - parseEnv.mockResolvedValue(Ok(defaultEnv)); + parseEnv.mockResolvedValue(defaultEnv); const getUpmConfigPath = mockService(); - getUpmConfigPath.mockReturnValue(AsyncOk(exampleUpmConfigPath)); + getUpmConfigPath.mockResolvedValue(exampleUpmConfigPath); const login = mockService(); - login.mockReturnValue(AsyncOk()); + login.mockResolvedValue(undefined); const log = makeMockLogger(); @@ -45,67 +35,8 @@ describe("cmd-login", () => { return { loginCmd, parseEnv, getUpmConfigPath, login, log } as const; } - it("should fail if env could not be parsed", async () => { - const expected = new GenericIOError("Read"); - const { loginCmd, parseEnv } = makeDependencies(); - parseEnv.mockResolvedValue(Err(expected)); - - const resultCode = await loginCmd({ _global: {} }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - // TODO: Add tests for prompting logic - it("should fail if upm config path could not be determined", async () => { - const expected = new RequiredEnvMissingError([]); - const { loginCmd, getUpmConfigPath } = makeDependencies(); - getUpmConfigPath.mockReturnValue(AsyncErr(expected)); - - const resultCode = await loginCmd({ - username: exampleUser, - password: examplePassword, - email: exampleEmail, - _global: { registry: exampleRegistryUrl }, - }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - - it("should fail if login failed", async () => { - const expected = new RequiredEnvMissingError([]); - const { loginCmd, login } = makeDependencies(); - login.mockReturnValue(AsyncErr(expected)); - - const resultCode = await loginCmd({ - username: exampleUser, - password: examplePassword, - email: exampleEmail, - _global: { registry: exampleRegistryUrl }, - }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - - it("should notify if unauthorized", async () => { - const { loginCmd, login, log } = makeDependencies(); - login.mockReturnValue( - Err(new RegistryAuthenticationError()).toAsyncResult() - ); - - await loginCmd({ - username: exampleUser, - password: examplePassword, - email: exampleEmail, - _global: { registry: exampleRegistryUrl }, - }); - - expect(log.warn).toHaveBeenCalledWith( - "401", - "Incorrect username or password" - ); - }); - it("should notify of success", async () => { const { loginCmd, log } = makeDependencies(); diff --git a/test/cli/cmd-remove.test.ts b/test/cli/cmd-remove.test.ts index 7ffd5724..53465114 100644 --- a/test/cli/cmd-remove.test.ts +++ b/test/cli/cmd-remove.test.ts @@ -1,13 +1,10 @@ import { exampleRegistryUrl } from "../domain/data-registry"; import { Env, ParseEnv } from "../../src/services/parse-env"; import { makeRemoveCmd } from "../../src/cli/cmd-remove"; -import { Err, Ok } from "ts-results-es"; import { makeDomainName } from "../../src/domain/domain-name"; import { SemanticVersion } from "../../src/domain/semantic-version"; import { makeMockLogger } from "./log.mock"; import { mockService } from "../services/service.mock"; -import { GenericIOError } from "../../src/io/common-errors"; -import { ResultCodes } from "../../src/cli/result-codes"; import { RemovePackages } from "../../src/services/remove-packages"; import { AsyncOk } from "../../src/utils/result-utils"; @@ -19,7 +16,7 @@ const defaultEnv = { function makeDependencies() { const parseEnv = mockService(); - parseEnv.mockResolvedValue(Ok(defaultEnv)); + parseEnv.mockResolvedValue(defaultEnv); const removePackages = mockService(); removePackages.mockReturnValue( @@ -38,27 +35,6 @@ function makeDependencies() { } describe("cmd-remove", () => { - it("should fail if env could not be parsed", async () => { - const expected = new GenericIOError("Read"); - const { removeCmd, parseEnv } = makeDependencies(); - parseEnv.mockResolvedValue(Err(expected)); - - const resultCode = await removeCmd([somePackage], { _global: {} }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - - it("should fail if package could not be removed", async () => { - const { removeCmd, removePackages } = makeDependencies(); - removePackages.mockReturnValue( - Err(new GenericIOError("Read")).toAsyncResult() - ); - - const resultCode = await removeCmd([somePackage], { _global: {} }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - it("should print removed packages", async () => { const { removeCmd, log } = makeDependencies(); diff --git a/test/cli/cmd-search.test.ts b/test/cli/cmd-search.test.ts index f74a3d7e..505ce8b6 100644 --- a/test/cli/cmd-search.test.ts +++ b/test/cli/cmd-search.test.ts @@ -4,14 +4,11 @@ import { makeSemanticVersion } from "../../src/domain/semantic-version"; import { makeMockLogger } from "./log.mock"; import { SearchedPackument } from "../../src/io/npm-search"; import { exampleRegistryUrl } from "../domain/data-registry"; -import { Ok } from "ts-results-es"; 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"; -import { GenericNetworkError } from "../../src/io/common-errors"; -import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; const exampleSearchResult: SearchedPackument = { name: makeDomainName("com.example.package-a"), @@ -23,12 +20,12 @@ const exampleSearchResult: SearchedPackument = { function makeDependencies() { const parseEnv = mockService(); - parseEnv.mockResolvedValue( - Ok({ registry: { url: exampleRegistryUrl, auth: null } } as Env) - ); + parseEnv.mockResolvedValue({ + registry: { url: exampleRegistryUrl, auth: null }, + } as Env); const searchPackages = mockService(); - searchPackages.mockReturnValue(AsyncOk([exampleSearchResult])); + searchPackages.mockResolvedValue([exampleSearchResult]); const log = makeMockLogger(); @@ -74,7 +71,7 @@ describe("cmd-search", () => { it("should notify of unknown packument", async () => { const { searchCmd, searchPackages, log } = makeDependencies(); - searchPackages.mockReturnValue(AsyncOk([])); + searchPackages.mockResolvedValue([]); await searchCmd("pkg-not-exist", options); @@ -84,35 +81,12 @@ describe("cmd-search", () => { ); }); - it("should fail if packuments could not be searched", async () => { - const expected = new GenericNetworkError(); - const { searchCmd, searchPackages } = makeDependencies(); - searchPackages.mockReturnValue(AsyncErr(expected)); - - const resultCode = await searchCmd("package-a", options); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - - it("should notify if packuments could not be searched", async () => { - const expected = new GenericNetworkError(); - const { searchCmd, searchPackages, log } = makeDependencies(); - searchPackages.mockReturnValue(AsyncErr(expected)); - - await searchCmd("package-a", options); - - expect(log.warn).toHaveBeenCalledWith( - "", - "/-/all endpoint is not available" - ); - }); - it("should notify when falling back to old search", async () => { const { searchCmd, searchPackages, log } = makeDependencies(); searchPackages.mockImplementation( - (_registry, _keyword, onUseAllFallback) => { + async (_registry, _keyword, onUseAllFallback) => { onUseAllFallback && onUseAllFallback(); - return AsyncOk([]); + return []; } ); diff --git a/test/cli/cmd-view.test.ts b/test/cli/cmd-view.test.ts index ee5e0c62..c33d83c2 100644 --- a/test/cli/cmd-view.test.ts +++ b/test/cli/cmd-view.test.ts @@ -2,20 +2,15 @@ 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 { Err, Ok } from "ts-results-es"; import { makeDomainName } from "../../src/domain/domain-name"; import { makePackageReference } from "../../src/domain/package-reference"; import { makeSemanticVersion } from "../../src/domain/semantic-version"; import { makeMockLogger } from "./log.mock"; import { buildPackument } from "../domain/data-packument"; import { mockService } from "../services/service.mock"; -import { - GenericIOError, - GenericNetworkError, -} from "../../src/io/common-errors"; import { ResultCodes } from "../../src/cli/result-codes"; -import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; import { FetchPackument } from "../../src/io/packument-io"; +import { PackumentNotFoundError } from "../../src/common-errors"; const somePackage = makeDomainName("com.some.package"); const somePackument = buildPackument(somePackage, (packument) => @@ -56,10 +51,10 @@ const defaultEnv = { function makeDependencies() { const parseEnv = mockService(); - parseEnv.mockResolvedValue(Ok(defaultEnv)); + parseEnv.mockResolvedValue(defaultEnv); const fetchPackument = mockService(); - fetchPackument.mockReturnValue(AsyncOk(somePackument)); + fetchPackument.mockResolvedValue(somePackument); const log = makeMockLogger(); @@ -73,16 +68,6 @@ function makeDependencies() { } describe("cmd-view", () => { - it("should fail if env could not be parsed", async () => { - const expected = new GenericIOError("Read"); - const { viewCmd, parseEnv } = makeDependencies(); - parseEnv.mockResolvedValue(Err(expected)); - - const resultCode = await viewCmd(somePackage, { _global: {} }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - it("should fail if package version was specified", async () => { const { viewCmd } = makeDependencies(); @@ -94,6 +79,15 @@ describe("cmd-view", () => { expect(resultCode).toEqual(ResultCodes.Error); }); + it("should fail if package is not found", async () => { + const { viewCmd, fetchPackument } = makeDependencies(); + fetchPackument.mockResolvedValue(null); + + await expect(viewCmd(somePackage, { _global: {} })).rejects.toBeInstanceOf( + PackumentNotFoundError + ); + }); + it("should notify if package version was specified", async () => { const { viewCmd, log } = makeDependencies(); @@ -108,37 +102,6 @@ describe("cmd-view", () => { ); }); - it("should fail if package was not found", async () => { - const { viewCmd, fetchPackument } = makeDependencies(); - fetchPackument.mockReturnValue(AsyncOk(null)); - - const resultCode = await viewCmd(somePackage, { _global: {} }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - - it("should fail if package could not be resolved", async () => { - const expected = new GenericNetworkError(); - const { viewCmd, fetchPackument } = makeDependencies(); - fetchPackument.mockReturnValue(AsyncErr(expected)); - - const resultCode = await viewCmd(somePackage, { _global: {} }); - - expect(resultCode).toEqual(ResultCodes.Error); - }); - - it("should notify if package could not be resolved", async () => { - const { viewCmd, fetchPackument, log } = makeDependencies(); - fetchPackument.mockReturnValue(AsyncOk(null)); - - await viewCmd(somePackage, { _global: {} }); - - expect(log.error).toHaveBeenCalledWith( - "404", - expect.stringContaining("not found") - ); - }); - it("should print package information", async () => { const consoleSpy = jest.spyOn(console, "log"); const { viewCmd } = makeDependencies(); diff --git a/test/domain/package-manifest.test.ts b/test/domain/package-manifest.test.ts index 3f40a11e..177add2e 100644 --- a/test/domain/package-manifest.test.ts +++ b/test/domain/package-manifest.test.ts @@ -1,9 +1,9 @@ import { dependenciesOf, - InvalidTargetEditorError, tryGetTargetEditorVersionFor, } 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", () => { @@ -41,9 +41,9 @@ describe("package manifest", () => { unity: "2020.3", } as const; - const result = tryGetTargetEditorVersionFor(packageManifest); + const actual = tryGetTargetEditorVersionFor(packageManifest); - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); + expect(actual).toEqual(expected); }); it("should get version with release", () => { @@ -53,17 +53,17 @@ describe("package manifest", () => { unityRelease: "1", } as const; - const result = tryGetTargetEditorVersionFor(packageManifest); + const actual = tryGetTargetEditorVersionFor(packageManifest); - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); + expect(actual).toEqual(expected); }); it("should be null for missing major.minor", () => { const packageManifest = {}; - const result = tryGetTargetEditorVersionFor(packageManifest); + const actual = tryGetTargetEditorVersionFor(packageManifest); - expect(result).toBeOk((actual) => expect(actual).toBeNull()); + expect(actual).toBeNull(); }); it("should fail for bad version", () => { @@ -71,16 +71,14 @@ describe("package manifest", () => { unity: "bad version", } as const; - const result = tryGetTargetEditorVersionFor( - // We pass a package with a bad value on purpose here - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - packageManifest - ); - - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(InvalidTargetEditorError) - ); + expect(() => + tryGetTargetEditorVersionFor( + // We pass a package with a bad value on purpose here + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + packageManifest + ) + ).toThrow(MalformedPackumentError); }); }); }); diff --git a/test/domain/packument.test.ts b/test/domain/packument.test.ts index 74203e50..9118d9f5 100644 --- a/test/domain/packument.test.ts +++ b/test/domain/packument.test.ts @@ -80,11 +80,9 @@ describe("packument", () => { it("should fail it packument has no versions", () => { const emptyPackument = buildPackument(somePackage); - const result = tryResolvePackumentVersion(emptyPackument, "latest"); - - expect(result).toBeError((error) => - expect(error).toBeInstanceOf(NoVersionsError) - ); + expect(() => + tryResolvePackumentVersion(emptyPackument, "latest") + ).toThrow(NoVersionsError); }); it("should find latest version when requested", () => { diff --git a/test/e2e/add.e2e.ts b/test/e2e/add.e2e.ts index 9c2fddad..620eb250 100644 --- a/test/e2e/add.e2e.ts +++ b/test/e2e/add.e2e.ts @@ -207,9 +207,7 @@ describe("add packages", () => { expect(output.stdOut).toEqual([]); expect(output.stdErr).toEqual( expect.arrayContaining([ - expect.stringContaining( - `The package "does.not.exist" was not found in any of the provided registries.` - ), + expect.stringContaining(`Package "does.not.exist" could not be found.`), ]) ); }); @@ -230,7 +228,7 @@ describe("add packages", () => { expect(output.stdErr).toEqual( expect.arrayContaining([ expect.stringContaining( - `The package "dev.comradevanti.opt-unity" has no published version "100.1.1".` + `Can not add "dev.comradevanti.opt-unity" because version 100.1.1 could not be found in any registry.` ), ]) ); diff --git a/test/e2e/check/project-manifest.ts b/test/e2e/check/project-manifest.ts index 7e1e712c..d8abb463 100644 --- a/test/e2e/check/project-manifest.ts +++ b/test/e2e/check/project-manifest.ts @@ -1,13 +1,13 @@ import { UnityProjectManifest } from "../../../src/domain/project-manifest"; import { makeLoadProjectManifest } from "../../../src/io/project-manifest-io"; -import { makeReadText } from "../../../src/io/fs-result"; +import { makeReadText } from "../../../src/io/text-file-io"; import { noopLogger } from "../../../src/logging"; -const readText = makeReadText(noopLogger); -const loadProjectManifest = makeLoadProjectManifest(readText); +const readText = makeReadText(); +const loadProjectManifest = makeLoadProjectManifest(readText, noopLogger); export async function getProjectManifest( projectDirectory: string ): Promise { - return (await loadProjectManifest(projectDirectory).promise).unwrap(); + return await loadProjectManifest(projectDirectory); } diff --git a/test/e2e/setup/project.ts b/test/e2e/setup/project.ts index f9b7f831..a8897207 100644 --- a/test/e2e/setup/project.ts +++ b/test/e2e/setup/project.ts @@ -4,13 +4,12 @@ import { projectVersionTxtPathFor } from "../../../src/io/project-version-io"; import yaml from "yaml"; import { emptyProjectManifest } from "../../../src/domain/project-manifest"; import { makeWriteProjectManifest } from "../../../src/io/project-manifest-io"; -import { makeWriteText } from "../../../src/io/fs-result"; -import { noopLogger } from "../../../src/logging"; +import { makeWriteText } from "../../../src/io/text-file-io"; import { dropDirectory } from "./directories"; export type ProjectOptions = {}; -const writeFile = makeWriteText(noopLogger); +const writeFile = makeWriteText(); const writeProjectManifest = makeWriteProjectManifest(writeFile); export async function prepareUnityProject( @@ -32,7 +31,7 @@ export async function prepareUnityProject( ); const manifest = emptyProjectManifest; - (await writeProjectManifest(projectDir, manifest).promise).unwrap(); + await writeProjectManifest(projectDir, manifest); return projectDir; } diff --git a/test/io/all-packuments-io.test.ts b/test/io/all-packuments-io.test.ts index 85f060e1..dc52d125 100644 --- a/test/io/all-packuments-io.test.ts +++ b/test/io/all-packuments-io.test.ts @@ -4,10 +4,7 @@ import { Registry } from "../../src/domain/registry"; import { exampleRegistryUrl } from "../domain/data-registry"; import { HttpErrorBase } from "npm-registry-fetch/lib/errors"; import { noopLogger } from "../../src/logging"; -import { - GenericNetworkError, - RegistryAuthenticationError, -} from "../../src/io/common-errors"; +import { RegistryAuthenticationError } from "../../src/io/common-errors"; jest.mock("npm-registry-fetch"); @@ -31,11 +28,7 @@ describe("fetch all packuments", () => { jest.mocked(npmFetch.json).mockRejectedValue(expected); const { getAllPackuments } = makeDependencies(); - const result = await getAllPackuments(exampleRegistry).promise; - - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(GenericNetworkError) - ); + await expect(getAllPackuments(exampleRegistry)).rejects.toEqual(expected); }); it("should fail on auth error response", async () => { @@ -47,10 +40,8 @@ describe("fetch all packuments", () => { jest.mocked(npmFetch.json).mockRejectedValue(expected); const { getAllPackuments } = makeDependencies(); - const result = await getAllPackuments(exampleRegistry).promise; - - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(RegistryAuthenticationError) + await expect(getAllPackuments(exampleRegistry)).rejects.toBeInstanceOf( + RegistryAuthenticationError ); }); @@ -61,8 +52,8 @@ describe("fetch all packuments", () => { jest.mocked(npmFetch.json).mockResolvedValue(expected); const { getAllPackuments } = makeDependencies(); - const result = await getAllPackuments(exampleRegistry).promise; + const actual = await getAllPackuments(exampleRegistry); - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); + expect(actual).toEqual(expected); }); }); diff --git a/test/io/builtin-packages.test.ts b/test/io/builtin-packages.test.ts index 0f29014d..3303503d 100644 --- a/test/io/builtin-packages.test.ts +++ b/test/io/builtin-packages.test.ts @@ -1,6 +1,6 @@ import * as specialPaths from "../../src/io/special-paths"; import { OSNotSupportedError } from "../../src/io/special-paths"; -import * as fileIo from "../../src/io/fs-result"; +import * as directoryIO from "../../src/io/directory-io"; import { Err, Ok } from "ts-results-es"; import { EditorNotInstalledError, @@ -8,8 +8,7 @@ import { } from "../../src/io/builtin-packages"; import { makeEditorVersion } from "../../src/domain/editor-version"; import { noopLogger } from "../../src/logging"; -import { enoentError } from "./node-error.mock"; -import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; +import { eaccesError, enoentError } from "./node-error.mock"; function makeDependencies() { const getBuiltInPackages = makeFindBuiltInPackages(noopLogger); @@ -35,14 +34,26 @@ describe("builtin-packages", () => { const expected = new EditorNotInstalledError(version); const { getBuiltInPackages } = makeDependencies(); jest - .spyOn(fileIo, "tryGetDirectoriesIn") - .mockReturnValue(AsyncErr(enoentError)); + .spyOn(directoryIO, "tryGetDirectoriesIn") + .mockRejectedValue(enoentError); const result = await getBuiltInPackages(version).promise; expect(result).toBeError((actual) => expect(actual).toEqual(expected)); }); + it("should fail if directory could not be read", async () => { + const version = makeEditorVersion(2022, 1, 2, "f", 1); + const { getBuiltInPackages } = makeDependencies(); + const expected = eaccesError; + jest + .spyOn(specialPaths, "tryGetEditorInstallPath") + .mockReturnValue(Ok("/some/path")); + jest.spyOn(directoryIO, "tryGetDirectoriesIn").mockRejectedValue(expected); + + await expect(getBuiltInPackages(version).promise).rejects.toEqual(expected); + }); + it("should find package names", async () => { const version = makeEditorVersion(2022, 1, 2, "f", 1); const expected = ["com.unity.ugui", "com.unity.modules.uielements"]; @@ -50,9 +61,7 @@ describe("builtin-packages", () => { jest .spyOn(specialPaths, "tryGetEditorInstallPath") .mockReturnValue(Ok("/some/path")); - jest - .spyOn(fileIo, "tryGetDirectoriesIn") - .mockReturnValue(AsyncOk(expected)); + jest.spyOn(directoryIO, "tryGetDirectoriesIn").mockResolvedValue(expected); const result = await getBuiltInPackages(version).promise; diff --git a/test/io/check-url.test.ts b/test/io/check-url.test.ts index ab99c726..ef60548f 100644 --- a/test/io/check-url.test.ts +++ b/test/io/check-url.test.ts @@ -1,6 +1,5 @@ import { makeCheckUrlExists } from "../../src/io/check-url"; import fetch, { Response } from "node-fetch"; -import { GenericNetworkError } from "../../src/io/common-errors"; jest.mock("node-fetch"); @@ -16,46 +15,22 @@ describe("check url exists", () => { } as Response); const { checkUrlExists } = makeDependencies(); - const result = await checkUrlExists("https://some.url.com").promise; + const actual = await checkUrlExists("https://some.url.com"); - expect(result).toBeOk((actual) => expect(actual).toBeTruthy()); + expect(actual).toBeTruthy(); }); - it("should be false if url responds with 404", async () => { - jest.mocked(fetch).mockResolvedValue({ - status: 404, - } as Response); - const { checkUrlExists } = makeDependencies(); - - const result = await checkUrlExists("https://some.url.com").promise; - - expect(result).toBeOk((actual) => expect(actual).toBeFalsy()); - }); - - it.each([100, 201, 301, 401, 500])( - "should be fail for other status codes (%d)", + 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 { checkUrlExists } = makeDependencies(); - const result = await checkUrlExists("https://some.url.com").promise; + const actual = await checkUrlExists("https://some.url.com"); - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(GenericNetworkError) - ); + expect(actual).toBeFalsy(); } ); - - it("should be fail if request fails", async () => { - jest.mocked(fetch).mockRejectedValueOnce(new Error("Network bad")); - const { checkUrlExists } = makeDependencies(); - - const result = await checkUrlExists("https://some.url.com").promise; - - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(GenericNetworkError) - ); - }); }); diff --git a/test/io/directory-io.test.ts b/test/io/directory-io.test.ts new file mode 100644 index 00000000..1b1a5e1f --- /dev/null +++ b/test/io/directory-io.test.ts @@ -0,0 +1,43 @@ +import { makeNodeError } from "./node-error.mock"; +import fs from "fs/promises"; +import { tryGetDirectoriesIn } from "../../src/io/directory-io"; +import { Dirent } from "node:fs"; + +describe("directory io", () => { + describe("get directories", () => { + it("should fail if directory could not be read", async () => { + const expected = makeNodeError("EACCES"); + jest.spyOn(fs, "readdir").mockRejectedValue(expected); + + await expect(tryGetDirectoriesIn("/good/path/")).rejects.toEqual( + expected + ); + }); + + it("should get names of directories", async () => { + jest + .spyOn(fs, "readdir") + .mockResolvedValue([ + { name: "a", isDirectory: () => true } as Dirent, + { name: "b", isDirectory: () => true } as Dirent, + ]); + + const actual = await tryGetDirectoriesIn("/good/path/"); + + expect(actual).toEqual(["a", "b"]); + }); + + it("should get only directories", async () => { + jest + .spyOn(fs, "readdir") + .mockResolvedValue([ + { name: "a", isDirectory: () => true } as Dirent, + { name: "b.txt", isDirectory: () => false } as Dirent, + ]); + + const actual = await tryGetDirectoriesIn("/good/path/"); + + expect(actual).toEqual(["a"]); + }); + }); +}); diff --git a/test/io/fs-result.test.ts b/test/io/fs-result.test.ts deleted file mode 100644 index d5032b49..00000000 --- a/test/io/fs-result.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import fs from "fs/promises"; -import { - makeReadText, - makeWriteText, - tryGetDirectoriesIn, -} from "../../src/io/fs-result"; -import fse from "fs-extra"; -import { Dirent } from "node:fs"; -import { noopLogger } from "../../src/logging"; -import { makeNodeError } from "./node-error.mock"; - -describe("fs-result", () => { - describe("read text", () => { - function makeDependencies() { - const readFile = makeReadText(noopLogger); - - return { readFile } as const; - } - - it("should produce text if file can be read", async () => { - const { readFile } = makeDependencies(); - const expected = "content"; - jest.spyOn(fs, "readFile").mockResolvedValue(expected); - - const result = await readFile("path/to/file.txt").promise; - - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); - }); - - it("should fail if file could not be read", async () => { - const { readFile } = makeDependencies(); - const expected = makeNodeError("ENOENT"); - jest.spyOn(fs, "readFile").mockRejectedValue(expected); - - const result = await readFile("path/to/file.txt").promise; - - expect(result).toBeError((error) => expect(error).toEqual(expected)); - }); - }); - - describe("write text", () => { - function makeDependencies() { - const writeFile = makeWriteText(noopLogger); - - return { writeFile } as const; - } - - beforeEach(() => { - jest.spyOn(fse, "ensureDir").mockResolvedValue(undefined!); - jest.spyOn(fs, "writeFile").mockResolvedValue(undefined); - }); - - it("should be ok for valid write", async () => { - const { writeFile } = makeDependencies(); - - const result = await writeFile("path/to/file.txt", "content").promise; - - expect(result).toBeOk(); - }); - - it("should write to correct path", async () => { - const { writeFile } = makeDependencies(); - const expected = "path/to/file.txt"; - const fsWrite = jest.spyOn(fs, "writeFile"); - - await writeFile(expected, "content").promise; - - expect(fsWrite).toHaveBeenCalledWith(expected, expect.any(String)); - }); - - it("should write text to file", async () => { - const { writeFile } = makeDependencies(); - const expected = "content"; - const fsWrite = jest.spyOn(fs, "writeFile"); - - await writeFile("path/to/file.txt", expected).promise; - - expect(fsWrite).toHaveBeenCalledWith(expect.any(String), expected); - }); - - it("should ensure directory exists", async () => { - const { writeFile } = makeDependencies(); - const fseEnsureDir = jest.spyOn(fse, "ensureDir"); - - await writeFile("/path/to/file.txt", "content").promise; - - expect(fseEnsureDir).toHaveBeenCalledWith("/path/to"); - }); - - it("should notify of directory ensure error", async () => { - const { writeFile } = makeDependencies(); - const expected = makeNodeError("EACCES"); - jest.spyOn(fse, "ensureDir").mockRejectedValue(expected as never); - - const result = await writeFile("/path/to/file.txt", "content").promise; - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); - }); - - it("should notify of write error", async () => { - const { writeFile } = makeDependencies(); - const expected = makeNodeError("EACCES"); - jest.spyOn(fs, "writeFile").mockRejectedValue(expected as never); - - const result = await writeFile("/path/to/file.txt", "content").promise; - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); - }); - }); - - describe("get directories", () => { - it("should fail if directory could not be read", async () => { - const expected = makeNodeError("EACCES"); - jest.spyOn(fs, "readdir").mockRejectedValue(expected); - - const result = await tryGetDirectoriesIn("/good/path/", noopLogger) - .promise; - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); - }); - - it("should get names of directories", async () => { - jest - .spyOn(fs, "readdir") - .mockResolvedValue([ - { name: "a", isDirectory: () => true } as Dirent, - { name: "b", isDirectory: () => true } as Dirent, - ]); - - const result = await tryGetDirectoriesIn("/good/path/", noopLogger) - .promise; - - expect(result).toBeOk((actual) => expect(actual).toEqual(["a", "b"])); - }); - - it("should get only directories", async () => { - jest - .spyOn(fs, "readdir") - .mockResolvedValue([ - { name: "a", isDirectory: () => true } as Dirent, - { name: "b.txt", isDirectory: () => false } as Dirent, - ]); - - const result = await tryGetDirectoriesIn("/good/path/", noopLogger) - .promise; - - expect(result).toBeOk((actual) => expect(actual).toEqual(["a"])); - }); - }); -}); diff --git a/test/io/node-error.mock.ts b/test/io/node-error.mock.ts index 6b20af61..d48667b5 100644 --- a/test/io/node-error.mock.ts +++ b/test/io/node-error.mock.ts @@ -4,6 +4,7 @@ */ export function makeNodeError(code: string): NodeJS.ErrnoException { const error = new Error() as NodeJS.ErrnoException; + error.errno = 123; error.code = code; return error; } diff --git a/test/io/npm-search.test.ts b/test/io/npm-search.test.ts index 6234d1f7..1f9ec7e6 100644 --- a/test/io/npm-search.test.ts +++ b/test/io/npm-search.test.ts @@ -5,10 +5,7 @@ import { makeSearchRegistry } from "../../src/io/npm-search"; import { Registry } from "../../src/domain/registry"; import { exampleRegistryUrl } from "../domain/data-registry"; import { noopLogger } from "../../src/logging"; -import { - GenericNetworkError, - RegistryAuthenticationError, -} from "../../src/io/common-errors"; +import { RegistryAuthenticationError } from "../../src/io/common-errors"; jest.mock("libnpmsearch"); @@ -32,10 +29,8 @@ describe("npm search", () => { jest.mocked(npmSearch).mockRejectedValue(expected); const { searchRegistry } = makeDependencies(); - const result = await searchRegistry(exampleRegistry, "wow").promise; - - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(GenericNetworkError) + await expect(searchRegistry(exampleRegistry, "wow")).rejects.toEqual( + expected ); }); @@ -48,10 +43,8 @@ describe("npm search", () => { jest.mocked(npmSearch).mockRejectedValue(expected); const { searchRegistry } = makeDependencies(); - const result = await searchRegistry(exampleRegistry, "wow").promise; - - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(RegistryAuthenticationError) + await expect(searchRegistry(exampleRegistry, "wow")).rejects.toBeInstanceOf( + RegistryAuthenticationError ); }); @@ -60,8 +53,8 @@ describe("npm search", () => { jest.mocked(npmSearch).mockResolvedValue(expected); const { searchRegistry } = makeDependencies(); - const result = await searchRegistry(exampleRegistry, "wow").promise; + const actual = await searchRegistry(exampleRegistry, "wow"); - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); + expect(actual).toEqual(expected); }); }); diff --git a/test/io/npmrc-io.test.ts b/test/io/npmrc-io.test.ts index cb8ae350..aadd0d54 100644 --- a/test/io/npmrc-io.test.ts +++ b/test/io/npmrc-io.test.ts @@ -1,17 +1,13 @@ -import { ReadTextFile, WriteTextFile } from "../../src/io/fs-result"; -import { AsyncResult, Err, Ok } from "ts-results-es"; +import { ReadTextFile, WriteTextFile } from "../../src/io/text-file-io"; import { EOL } from "node:os"; import { - makeLoadNpmrc, makeFindNpmrcPath, + makeLoadNpmrc, makeSaveNpmrc, } from "../../src/io/npmrc-io"; import path from "path"; import { GetHomePath } from "../../src/io/special-paths"; -import { RequiredEnvMissingError } from "../../src/io/upm-config-io"; import { mockService } from "../services/service.mock"; -import { eaccesError, enoentError } from "./node-error.mock"; -import { GenericIOError } from "../../src/io/common-errors"; describe("npmrc-io", () => { describe("get path", () => { @@ -27,20 +23,11 @@ describe("npmrc-io", () => { const { findPath, getHomePath } = makeDependencies(); const home = path.join(path.sep, "user", "dir"); const expected = path.join(home, ".npmrc"); - getHomePath.mockReturnValue(Ok(home)); - - const result = findPath(); - - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); - }); - - it("should fail if home could not be determined", () => { - const { findPath, getHomePath } = makeDependencies(); - getHomePath.mockReturnValue(Err(new RequiredEnvMissingError([]))); + getHomePath.mockReturnValue(home); - const result = findPath(); + const actual = findPath(); - expect(result).toBeError(); + expect(actual).toEqual(expected); }); }); @@ -55,53 +42,42 @@ describe("npmrc-io", () => { it("should be ok for valid file", async () => { const fileContent = `key1 = value1${EOL}key2 = "value2"`; const { loadNpmrc, readText } = makeDependencies(); - readText.mockReturnValue(new AsyncResult(Ok(fileContent))); + readText.mockResolvedValue(fileContent); - const result = await loadNpmrc("/valid/path/.npmrc").promise; + const actual = await loadNpmrc("/valid/path/.npmrc"); - expect(result).toBeOk((actual) => - expect(actual).toEqual(["key1 = value1", 'key2 = "value2"']) - ); + expect(actual).toEqual(["key1 = value1", 'key2 = "value2"']); }); it("should be null for missing file", async () => { const { loadNpmrc, readText } = makeDependencies(); const path = "/invalid/path/.npmrc"; - readText.mockReturnValue(new AsyncResult(Err(enoentError))); + readText.mockResolvedValue(null); - const result = await loadNpmrc(path).promise; + const actual = await loadNpmrc(path); - expect(result).toBeOk((actual) => expect(actual).toBeNull()); + expect(actual).toBeNull(); }); }); describe("save", () => { function makeDependencies() { const writeFile = mockService(); - writeFile.mockReturnValue(new AsyncResult(Ok(undefined))); + writeFile.mockResolvedValue(undefined); const saveNpmrc = makeSaveNpmrc(writeFile); return { saveNpmrc, writeFile } as const; } - it("should be ok when write succeeds", async () => { - const { saveNpmrc } = makeDependencies(); - const path = "/valid/path/.npmrc"; - - const result = await saveNpmrc(path, ["key=value"]).promise; - - expect(result).toBeOk(); - }); - - it("should fail when write fails", async () => { + it("should write joined lines", async () => { const { saveNpmrc, writeFile } = makeDependencies(); - const path = "/invalid/path/.npmrc"; - writeFile.mockReturnValue(new AsyncResult(Err(eaccesError))); + const path = "/valid/path/.npmrc"; - const result = await saveNpmrc(path, ["key=value"]).promise; + await saveNpmrc(path, ["key=value", "key2=value2"]); - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(GenericIOError) + expect(writeFile).toHaveBeenCalledWith( + path, + `key=value${EOL}key2=value2` ); }); }); diff --git a/test/io/packument-io.test.ts b/test/io/packument-io.test.ts index dd8897fb..d2b0acd8 100644 --- a/test/io/packument-io.test.ts +++ b/test/io/packument-io.test.ts @@ -6,6 +6,7 @@ import { Registry } from "../../src/domain/registry"; import { exampleRegistryUrl } from "../domain/data-registry"; import { mockRegClientGetResult } from "../services/registry-client.mock"; import { makeFetchPackument } from "../../src/io/packument-io"; +import { noopLogger } from "../../src/logging"; describe("packument io", () => { describe("fetch", () => { @@ -21,7 +22,7 @@ describe("packument io", () => { get: jest.fn(), }; - const fetchPackument = makeFetchPackument(regClient); + const fetchPackument = makeFetchPackument(regClient, noopLogger); return { fetchPackument, regClient } as const; } it("should get existing packument", async () => { @@ -30,9 +31,9 @@ describe("packument io", () => { const { fetchPackument, regClient } = makeDependencies(); mockRegClientGetResult(regClient, null, packument); - const result = await fetchPackument(exampleRegistry, packageA).promise; + const actual = await fetchPackument(exampleRegistry, packageA); - expect(result).toBeOk((actual) => expect(actual).toEqual(packument)); + expect(actual).toEqual(packument); }); it("should not find unknown packument", async () => { @@ -47,9 +48,9 @@ describe("packument io", () => { null ); - const result = await fetchPackument(exampleRegistry, packageA).promise; + const actual = await fetchPackument(exampleRegistry, packageA); - expect(result).toBeOk((actual) => expect(actual).toBeNull()); + expect(actual).toBeNull(); }); it("should fail for errors", async () => { @@ -64,9 +65,9 @@ describe("packument io", () => { null ); - const result = await fetchPackument(exampleRegistry, packageA).promise; - - expect(result).toBeError(); + await expect( + fetchPackument(exampleRegistry, packageA) + ).rejects.toBeInstanceOf(Error); }); }); }); diff --git a/test/io/project-manifest-io.mock.ts b/test/io/project-manifest-io.mock.ts deleted file mode 100644 index 76188750..00000000 --- a/test/io/project-manifest-io.mock.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - LoadProjectManifest, - makeProjectManifestMissingError, - manifestPathFor, - ManifestWriteError, - WriteProjectManifest, -} from "../../src/io/project-manifest-io"; -import { UnityProjectManifest } from "../../src/domain/project-manifest"; -import { Err, Ok } from "ts-results-es"; -import { AsyncOk } from "../../src/utils/result-utils"; - -/** - * Mocks results for a {@link LoadProjectManifest} function. - * @param loadProjectManifest The load function. - * @param manifest The manifest that should be returned. Null if there should - * be no manifest. - */ -export function mockProjectManifest( - loadProjectManifest: jest.MockedFunction, - manifest: UnityProjectManifest | null -) { - return loadProjectManifest.mockImplementation((projectPath) => { - const manifestPath = manifestPathFor(projectPath); - return manifest === null - ? Err(makeProjectManifestMissingError(manifestPath)).toAsyncResult() - : AsyncOk(manifest); - }); -} - -/** - * Mocks the result of writing the project manifest. - * @param writeProjectManifest The write function. - * @param error The error that should be returned by the write operation. If - * omitted the operation will be mocked to succeed. - */ -export function mockProjectManifestWriteResult( - writeProjectManifest: jest.MockedFunction, - error?: ManifestWriteError -) { - const result = error !== undefined ? Err(error) : Ok(undefined); - return writeProjectManifest.mockReturnValue(result.toAsyncResult()); -} diff --git a/test/io/project-manifest-io.test.ts b/test/io/project-manifest-io.test.ts index 43b474b0..28349d2c 100644 --- a/test/io/project-manifest-io.test.ts +++ b/test/io/project-manifest-io.test.ts @@ -1,23 +1,19 @@ import { makeLoadProjectManifest, makeWriteProjectManifest, + ManifestMalformedError, + ManifestMissingError, manifestPathFor, } from "../../src/io/project-manifest-io"; -import { - emptyProjectManifest, - mapScopedRegistry, -} from "../../src/domain/project-manifest"; +import { mapScopedRegistry } from "../../src/domain/project-manifest"; import path from "path"; -import { ReadTextFile, WriteTextFile } from "../../src/io/fs-result"; +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 { eaccesError, enoentError } from "./node-error.mock"; -import { FileMissingError, GenericIOError } from "../../src/io/common-errors"; -import { StringFormatError } from "../../src/utils/string-parsing"; -import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; +import { noopLogger } from "../../src/logging"; const exampleProjectPath = "/some/path"; describe("project-manifest io", () => { @@ -37,83 +33,62 @@ describe("project-manifest io", () => { function makeDependencies() { const readFile = mockService(); - const loadProjectManifest = makeLoadProjectManifest(readFile); + const loadProjectManifest = makeLoadProjectManifest(readFile, noopLogger); return { loadProjectManifest, readFile } as const; } - it("should fail if file could not be read", async () => { + it("should fail if file is missing", async () => { const { loadProjectManifest, readFile } = makeDependencies(); - readFile.mockReturnValue(AsyncErr(eaccesError)); - - const result = await loadProjectManifest(exampleProjectPath).promise; + readFile.mockResolvedValue(null); - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(GenericIOError) - ); + await expect( + loadProjectManifest(exampleProjectPath) + ).rejects.toBeInstanceOf(ManifestMissingError); }); - it("should fail if file is missing", async () => { + it("should fail if manifest contains invalid json", async () => { const { loadProjectManifest, readFile } = makeDependencies(); - readFile.mockReturnValue(AsyncErr(enoentError)); + readFile.mockResolvedValue("not {} valid : json"); - const result = await loadProjectManifest(exampleProjectPath).promise; - - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(FileMissingError) - ); + await expect( + loadProjectManifest(exampleProjectPath) + ).rejects.toBeInstanceOf(ManifestMalformedError); }); - it("should fail if file does not contain json", async () => { + it("should fail if manifest contains invalid content", async () => { const { loadProjectManifest, readFile } = makeDependencies(); - readFile.mockReturnValue(AsyncOk("{} dang, this is not json []")); + readFile.mockResolvedValue(`123`); - const result = await loadProjectManifest(exampleProjectPath).promise; - - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(StringFormatError) - ); + await expect( + loadProjectManifest(exampleProjectPath) + ).rejects.toBeInstanceOf(ManifestMalformedError); }); it("should load valid manifest", async () => { const { loadProjectManifest, readFile } = makeDependencies(); - readFile.mockReturnValue( - AsyncOk(`{ "dependencies": { "com.package.a": "1.0.0"} }`) + readFile.mockResolvedValue( + `{ "dependencies": { "com.package.a": "1.0.0"} }` ); - const result = await loadProjectManifest(exampleProjectPath).promise; + const actual = await loadProjectManifest(exampleProjectPath); - expect(result).toBeOk((actual) => - expect(actual).toEqual({ - dependencies: { - "com.package.a": "1.0.0", - }, - }) - ); + expect(actual).toEqual({ + dependencies: { + "com.package.a": "1.0.0", + }, + }); }); }); describe("write", () => { function makeDependencies() { const writeFile = mockService(); - writeFile.mockReturnValue(AsyncOk()); + writeFile.mockResolvedValue(undefined); const writeProjectManifest = makeWriteProjectManifest(writeFile); return { writeProjectManifest, writeFile } as const; } - it("should fail if file could not be written", async () => { - const expected = eaccesError; - const { writeProjectManifest, writeFile } = makeDependencies(); - writeFile.mockReturnValue(AsyncErr(expected)); - - const result = await writeProjectManifest( - exampleProjectPath, - emptyProjectManifest - ).promise; - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); - }); - it("should write manifest json", async () => { const { writeProjectManifest, writeFile } = makeDependencies(); @@ -121,7 +96,7 @@ describe("project-manifest io", () => { manifest.addDependency("com.package.a", "1.0.0", true, true) ); - await writeProjectManifest(exampleProjectPath, manifest).promise; + await writeProjectManifest(exampleProjectPath, manifest); expect(writeFile).toHaveBeenCalledWith( expect.any(String), @@ -156,7 +131,7 @@ describe("project-manifest io", () => { return removeScope(registry!, testDomain); }); - await writeProjectManifest(exampleProjectPath, manifest).promise; + await writeProjectManifest(exampleProjectPath, manifest); expect(writeFile).toHaveBeenCalledWith( expect.any(String), diff --git a/test/io/project-version-io.mock.ts b/test/io/project-version-io.mock.ts index d85c0d93..82e54362 100644 --- a/test/io/project-version-io.mock.ts +++ b/test/io/project-version-io.mock.ts @@ -3,7 +3,6 @@ import { stringifyEditorVersion, } from "../../src/domain/editor-version"; import { LoadProjectVersion } from "../../src/io/project-version-io"; -import { AsyncOk } from "../../src/utils/result-utils"; /** * Mocks return values for calls to a {@link LoadProjectVersion} function. @@ -20,5 +19,5 @@ export function mockProjectVersion( ? editorVersion : stringifyEditorVersion(editorVersion); - loadProjectVersion.mockReturnValue(AsyncOk(versionString)); + loadProjectVersion.mockResolvedValue(versionString); } diff --git a/test/io/project-version-io.test.ts b/test/io/project-version-io.test.ts index b2ffbc82..108dc03f 100644 --- a/test/io/project-version-io.test.ts +++ b/test/io/project-version-io.test.ts @@ -1,77 +1,57 @@ -import { ReadTextFile } from "../../src/io/fs-result"; -import { StringFormatError } from "../../src/utils/string-parsing"; +import { ReadTextFile } from "../../src/io/text-file-io"; import { mockService } from "../services/service.mock"; -import { makeLoadProjectVersion } from "../../src/io/project-version-io"; -import { eaccesError, enoentError } from "./node-error.mock"; import { - FileMissingError, - FileParseError, - GenericIOError, -} from "../../src/io/common-errors"; -import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; + makeLoadProjectVersion, + ProjectVersionMalformedError, + ProjectVersionMissingError, +} from "../../src/io/project-version-io"; +import { noopLogger } from "../../src/logging"; describe("project-version-io", () => { describe("load", () => { function makeDependencies() { const readFile = mockService(); - const loadProjectVersion = makeLoadProjectVersion(readFile); + const loadProjectVersion = makeLoadProjectVersion(readFile, noopLogger); return { loadProjectVersion, readFile } as const; } it("should fail if file is missing", async () => { const { loadProjectVersion, readFile } = makeDependencies(); - readFile.mockReturnValue(AsyncErr(enoentError)); + readFile.mockResolvedValue(null); - const result = await loadProjectVersion("/some/bad/path").promise; - - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(FileMissingError) - ); - }); - - it("should fail if file could not be read", async () => { - const { loadProjectVersion, readFile } = makeDependencies(); - readFile.mockReturnValue(AsyncErr(eaccesError)); - - const result = await loadProjectVersion("/some/bad/path").promise; - - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(GenericIOError) + await expect(loadProjectVersion("/some/bad/path")).rejects.toBeInstanceOf( + ProjectVersionMissingError ); }); it("should fail if file does not contain valid yaml", async () => { const { loadProjectVersion, readFile } = makeDependencies(); - readFile.mockReturnValue(AsyncOk("{ this is not valid yaml")); + readFile.mockResolvedValue("this\\ is { not } : yaml"); - const result = await loadProjectVersion("/some/path").promise; - - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(StringFormatError) + await expect(loadProjectVersion("/some/path")).rejects.toBeInstanceOf( + ProjectVersionMalformedError ); }); it("should fail if yaml does not contain editor-version", async () => { const { loadProjectVersion, readFile } = makeDependencies(); - readFile.mockReturnValue(AsyncOk("thisIsYaml: but not what we want")); - - const result = await loadProjectVersion("/some/path").promise; + readFile.mockResolvedValue("thisIsYaml: but not what we want"); - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(FileParseError) + await expect(loadProjectVersion("/some/path")).rejects.toBeInstanceOf( + ProjectVersionMalformedError ); }); it("should load valid version strings", async () => { const { loadProjectVersion, readFile } = makeDependencies(); const expected = "2022.1.2f1"; - readFile.mockReturnValue(AsyncOk(`m_EditorVersion: ${expected}`)); + readFile.mockResolvedValue(`m_EditorVersion: ${expected}`); - const result = await loadProjectVersion("/some/path").promise; + const actual = await loadProjectVersion("/some/path"); - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); + expect(actual).toEqual(expected); }); }); }); diff --git a/test/io/special-paths.test.ts b/test/io/special-paths.test.ts index 6bf45c37..91a99ec8 100644 --- a/test/io/special-paths.test.ts +++ b/test/io/special-paths.test.ts @@ -2,6 +2,7 @@ import path from "path"; import { tryGetEnv } from "../../src/utils/env-util"; import { makeGetHomePath, + NoHomePathError, OSNotSupportedError, tryGetEditorInstallPath, VersionNotSupportedOnOsError, @@ -27,9 +28,9 @@ describe("special-paths", () => { .mocked(tryGetEnv) .mockImplementation((key) => (key === "USERPROFILE" ? expected : null)); - const result = getHomePath(); + const actual = getHomePath(); - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); + expect(actual).toEqual(expected); }); it("should be HOME if USERPROFILE is not defined", () => { @@ -39,18 +40,16 @@ describe("special-paths", () => { .mocked(tryGetEnv) .mockImplementation((key) => (key === "HOME" ? expected : null)); - const result = getHomePath(); + const actual = getHomePath(); - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); + expect(actual).toEqual(expected); }); it("should fail if HOME and USERPROFILE are not defined", () => { const { getHomePath } = makeDependencies(); jest.mocked(tryGetEnv).mockReturnValue(null); - const result = getHomePath(); - - expect(result).toBeError(); + expect(() => getHomePath()).toThrow(NoHomePathError); }); }); diff --git a/test/io/text-file-io.test.ts b/test/io/text-file-io.test.ts new file mode 100644 index 00000000..7958ca6a --- /dev/null +++ b/test/io/text-file-io.test.ts @@ -0,0 +1,123 @@ +import fs from "fs/promises"; +import { makeReadText, makeWriteText } from "../../src/io/text-file-io"; +import fse from "fs-extra"; +import { makeNodeError } from "./node-error.mock"; + +describe("text file io", () => { + describe("read text", () => { + function makeDependencies() { + const readFile = makeReadText(); + + return { readFile } as const; + } + + it("should produce text if file can be read", async () => { + const { readFile } = makeDependencies(); + const expected = "content"; + jest.spyOn(fs, "readFile").mockResolvedValue(expected); + + const actual = await readFile("path/to/file.txt", false); + + expect(actual).toEqual(expected); + }); + + it("should be null if optional file is missing", async () => { + const { readFile } = makeDependencies(); + jest.spyOn(fs, "readFile").mockRejectedValue(makeNodeError("ENOENT")); + + const content = await readFile("path/to/file.txt", true); + + expect(content).toBeNull(); + }); + + it("should fail if non-optional file is missing", async () => { + const { readFile } = makeDependencies(); + const expected = makeNodeError("ENOENT"); + jest.spyOn(fs, "readFile").mockRejectedValue(expected); + + await expect(readFile("path/to/file.txt", false)).rejects.toEqual( + expected + ); + }); + + it("should fail if file could not be read", async () => { + const { readFile } = makeDependencies(); + const expected = makeNodeError("EACCES"); + jest.spyOn(fs, "readFile").mockRejectedValue(expected); + + await expect(readFile("path/to/file.txt", false)).rejects.toEqual( + expected + ); + }); + }); + + describe("write text", () => { + function makeDependencies() { + const writeFile = makeWriteText(); + + return { writeFile } as const; + } + + beforeEach(() => { + jest.spyOn(fse, "ensureDir").mockResolvedValue(undefined!); + jest.spyOn(fs, "writeFile").mockResolvedValue(undefined); + }); + + it("should be ok for valid write", async () => { + const { writeFile } = makeDependencies(); + + await expect( + writeFile("path/to/file.txt", "content") + ).resolves.toBeUndefined(); + }); + + it("should write to correct path", async () => { + const { writeFile } = makeDependencies(); + const expected = "path/to/file.txt"; + const fsWrite = jest.spyOn(fs, "writeFile"); + + await writeFile(expected, "content"); + + expect(fsWrite).toHaveBeenCalledWith(expected, expect.any(String)); + }); + + it("should write text to file", async () => { + const { writeFile } = makeDependencies(); + const expected = "content"; + const fsWrite = jest.spyOn(fs, "writeFile"); + + await writeFile("path/to/file.txt", expected); + + expect(fsWrite).toHaveBeenCalledWith(expect.any(String), expected); + }); + + it("should ensure directory exists", async () => { + const { writeFile } = makeDependencies(); + const fseEnsureDir = jest.spyOn(fse, "ensureDir"); + + await writeFile("/path/to/file.txt", "content"); + + expect(fseEnsureDir).toHaveBeenCalledWith("/path/to"); + }); + + it("should fail if directory could not be ensured", async () => { + const { writeFile } = makeDependencies(); + const expected = makeNodeError("EACCES"); + jest.spyOn(fse, "ensureDir").mockRejectedValue(expected as never); + + await expect(writeFile("/path/to/file.txt", "content")).rejects.toEqual( + expected + ); + }); + + it("should fail if file could not be written", async () => { + const { writeFile } = makeDependencies(); + const expected = makeNodeError("EACCES"); + jest.spyOn(fs, "writeFile").mockRejectedValue(expected as never); + + await expect(writeFile("/path/to/file.txt", "content")).rejects.toEqual( + expected + ); + }); + }); +}); diff --git a/test/io/upm-config-io.mock.ts b/test/io/upm-config-io.mock.ts deleted file mode 100644 index 87e282b3..00000000 --- a/test/io/upm-config-io.mock.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { UPMConfig } from "../../src/domain/upm-config"; -import { LoadUpmConfig } from "../../src/io/upm-config-io"; -import { AsyncOk } from "../../src/utils/result-utils"; - -/** - * Mocks return value for calls to {@link LoadUpmConfig} function. - * @param loadUpmConfig The function to mock. - * @param config The upm-config that should be returned. - */ -export function mockUpmConfig( - loadUpmConfig: jest.MockedFunction, - config: UPMConfig | null -) { - loadUpmConfig.mockReturnValue(AsyncOk(config)); -} diff --git a/test/io/upm-config-io.test.ts b/test/io/upm-config-io.test.ts index 7c50b592..22821f6e 100644 --- a/test/io/upm-config-io.test.ts +++ b/test/io/upm-config-io.test.ts @@ -1,17 +1,11 @@ import { makeGetUpmConfigPath, makeLoadUpmConfig, - RequiredEnvMissingError, } from "../../src/io/upm-config-io"; -import { Err, Ok } from "ts-results-es"; -import { ReadTextFile } from "../../src/io/fs-result"; +import { ReadTextFile } from "../../src/io/text-file-io"; import { mockService } from "../services/service.mock"; -import { StringFormatError } from "../../src/utils/string-parsing"; import { GetHomePath } from "../../src/io/special-paths"; import path from "path"; -import { eaccesError, enoentError } from "./node-error.mock"; -import { GenericIOError } from "../../src/io/common-errors"; -import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; import { RunChildProcess } from "../../src/io/child-process"; describe("upm-config-io", () => { @@ -33,21 +27,11 @@ describe("upm-config-io", () => { it("should be in home path", async () => { const { getUpmConfigPath, getHomePath } = makeDependencies(); const expected = path.resolve("/some/home/dir/.upmconfig.toml"); - getHomePath.mockReturnValue(Ok(path.dirname(expected))); + getHomePath.mockReturnValue(path.dirname(expected)); - const result = await getUpmConfigPath(false, false).promise; + const actual = await getUpmConfigPath(false, false); - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); - }); - - it("should fail if home could not be determined", async () => { - const { getUpmConfigPath, getHomePath } = makeDependencies(); - const expected = new RequiredEnvMissingError([]); - getHomePath.mockReturnValue(Err(expected)); - - const result = await getUpmConfigPath(false, false).promise; - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); + expect(actual).toEqual(expected); }); }); }); @@ -55,7 +39,7 @@ describe("upm-config-io", () => { describe("load", () => { function makeDependencies() { const readFile = mockService(); - readFile.mockReturnValue(AsyncOk("")); + readFile.mockResolvedValue(""); const loadUpmConfig = makeLoadUpmConfig(readFile); return { loadUpmConfig, readFile } as const; @@ -64,44 +48,20 @@ describe("upm-config-io", () => { it("should be null if file is not found", async () => { const { loadUpmConfig, readFile } = makeDependencies(); const path = "/home/user/.upmconfig.toml"; - readFile.mockReturnValue(AsyncErr(enoentError)); + readFile.mockResolvedValue(null); - const result = await loadUpmConfig(path).promise; + const actual = await loadUpmConfig(path); - expect(result).toBeOk((actual) => expect(actual).toBeNull()); + expect(actual).toBeNull(); }); it("should be parsed file", async () => { const { loadUpmConfig } = makeDependencies(); const path = "/home/user/.upmconfig.toml"; - const result = await loadUpmConfig(path).promise; - - expect(result).toBeOk((actual) => expect(actual).toEqual({})); - }); - - it("should fail if file could not be read", async () => { - const { loadUpmConfig, readFile } = makeDependencies(); - const path = "/home/user/.upmconfig.toml"; - readFile.mockReturnValue(AsyncErr(eaccesError)); + const actual = await loadUpmConfig(path); - const result = await loadUpmConfig(path).promise; - - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(GenericIOError) - ); - }); - - it("should fail if file has bad toml content", async () => { - const { loadUpmConfig, readFile } = makeDependencies(); - readFile.mockReturnValue(AsyncOk("This {\n is not]\n valid TOML")); - const path = "/home/user/.upmconfig.toml"; - - const result = await loadUpmConfig(path).promise; - - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(StringFormatError) - ); + expect(actual).toEqual({}); }); }); }); diff --git a/test/io/wsl.test.ts b/test/io/wsl.test.ts index f2395656..75fe199c 100644 --- a/test/io/wsl.test.ts +++ b/test/io/wsl.test.ts @@ -1,6 +1,5 @@ -import { ChildProcessError, RunChildProcess } from "../../src/io/child-process"; +import { RunChildProcess } from "../../src/io/child-process"; import { tryGetWslPath } from "../../src/io/wsl"; -import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; import { mockService } from "../services/service.mock"; jest.mock("is-wsl", () => ({ @@ -13,20 +12,11 @@ describe("wsl", () => { it("should be result of wslpath command", async () => { const expected = "/some/path"; const runChildProcess = mockService(); - runChildProcess.mockReturnValue(AsyncOk(expected)); + runChildProcess.mockResolvedValue(expected); - const result = await tryGetWslPath("SOMEVAR", runChildProcess).promise; + const actual = await tryGetWslPath("SOMEVAR", runChildProcess); - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); - }); - - it("should fail if wslpath fails", async () => { - const runChildProcess = mockService(); - runChildProcess.mockReturnValue(AsyncErr(new ChildProcessError())); - - const result = await tryGetWslPath("SOMEVAR", runChildProcess).promise; - - expect(result).toBeError(); + expect(actual).toEqual(expected); }); /* diff --git a/test/services/built-in-package-check.test.ts b/test/services/built-in-package-check.test.ts index a2aab1e0..47dcd70d 100644 --- a/test/services/built-in-package-check.test.ts +++ b/test/services/built-in-package-check.test.ts @@ -2,11 +2,9 @@ import { CheckIsUnityPackage } from "../../src/services/unity-package-check"; import { makeCheckIsBuiltInPackage } from "../../src/services/built-in-package-check"; import { mockService } from "./service.mock"; import { FetchPackument } from "../../src/io/packument-io"; -import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; import { makeDomainName } from "../../src/domain/domain-name"; import { makeSemanticVersion } from "../../src/domain/semantic-version"; import { UnityPackument } from "../../src/domain/packument"; -import { GenericNetworkError } from "../../src/io/common-errors"; describe("is built-in package", () => { const somePackage = makeDomainName("com.some.package"); @@ -26,61 +24,34 @@ describe("is built-in package", () => { it("should be false if package is not a Unity package", async () => { const { checkIsBuiltInPackage, checkIsUnityPackage } = makeDependencies(); - checkIsUnityPackage.mockReturnValue(AsyncOk(false)); + checkIsUnityPackage.mockResolvedValue(false); - const result = await checkIsBuiltInPackage(somePackage, someVersion) - .promise; + const actual = await checkIsBuiltInPackage(somePackage, someVersion); - expect(result).toBeOk((actual) => expect(actual).toBeFalsy()); + expect(actual).toBeFalsy(); }); it("should be false if package is Unity package and exists on Unity registry", async () => { const { checkIsBuiltInPackage, checkIsUnityPackage, fetchPackument } = makeDependencies(); - checkIsUnityPackage.mockReturnValue(AsyncOk(true)); - fetchPackument.mockReturnValue( - AsyncOk({ versions: { [someVersion]: {} } } as UnityPackument) - ); + checkIsUnityPackage.mockResolvedValue(true); + fetchPackument.mockResolvedValue({ + versions: { [someVersion]: {} }, + } as UnityPackument); - const result = await checkIsBuiltInPackage(somePackage, someVersion) - .promise; + const actual = await checkIsBuiltInPackage(somePackage, someVersion); - expect(result).toBeOk((actual) => expect(actual).toBeFalsy()); + expect(actual).toBeFalsy(); }); it("should be true if package is Unity package, but does not exist on Unity registry", async () => { const { checkIsBuiltInPackage, checkIsUnityPackage, fetchPackument } = makeDependencies(); - checkIsUnityPackage.mockReturnValue(AsyncOk(true)); - fetchPackument.mockReturnValue(AsyncOk(null)); - - const result = await checkIsBuiltInPackage(somePackage, someVersion) - .promise; - - expect(result).toBeOk((actual) => expect(actual).toBeTruthy()); - }); - - it("should fail if Unity package check failed", async () => { - const expected = new GenericNetworkError(); - const { checkIsBuiltInPackage, checkIsUnityPackage } = makeDependencies(); - checkIsUnityPackage.mockReturnValue(AsyncErr(expected)); - - const result = await checkIsBuiltInPackage(somePackage, someVersion) - .promise; - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); - }); - - it("should fail if Unity registry check failed", async () => { - const expected = new GenericNetworkError(); - const { checkIsBuiltInPackage, checkIsUnityPackage, fetchPackument } = - makeDependencies(); - checkIsUnityPackage.mockReturnValue(AsyncOk(true)); - fetchPackument.mockReturnValue(AsyncErr(expected)); + checkIsUnityPackage.mockResolvedValue(true); + fetchPackument.mockResolvedValue(null); - const result = await checkIsBuiltInPackage(somePackage, someVersion) - .promise; + const actual = await checkIsBuiltInPackage(somePackage, someVersion); - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); + expect(actual).toBeTruthy(); }); }); diff --git a/test/services/determine-editor-version.test.ts b/test/services/determine-editor-version.test.ts index 5ccfe3ee..e95d107d 100644 --- a/test/services/determine-editor-version.test.ts +++ b/test/services/determine-editor-version.test.ts @@ -3,8 +3,6 @@ import { mockService } from "./service.mock"; import { LoadProjectVersion } from "../../src/io/project-version-io"; import { makeEditorVersion } from "../../src/domain/editor-version"; import { mockProjectVersion } from "../io/project-version-io.mock"; -import { GenericIOError } from "../../src/io/common-errors"; -import { AsyncErr } from "../../src/utils/result-utils"; describe("determine editor version", () => { const exampleProjectPath = "/home/my-project/"; @@ -21,11 +19,9 @@ describe("determine editor version", () => { const { determineEditorVersion, loadProjectVersion } = makeDependencies(); mockProjectVersion(loadProjectVersion, "2021.3.1f1"); - const result = await determineEditorVersion(exampleProjectPath).promise; + const actual = await determineEditorVersion(exampleProjectPath); - expect(result).toBeOk((version) => - expect(version).toEqual(makeEditorVersion(2021, 3, 1, "f", 1)) - ); + expect(actual).toEqual(makeEditorVersion(2021, 3, 1, "f", 1)); }); it("should be original string for non-release versions", async () => { @@ -33,9 +29,9 @@ describe("determine editor version", () => { const expected = "2022.3"; mockProjectVersion(loadProjectVersion, expected); - const result = await determineEditorVersion(exampleProjectPath).promise; + const actual = await determineEditorVersion(exampleProjectPath); - expect(result).toBeOk((version) => expect(version).toEqual(expected)); + expect(actual).toEqual(expected); }); it("should be original string for non-version string", async () => { @@ -43,18 +39,8 @@ describe("determine editor version", () => { const expected = "Bad version"; mockProjectVersion(loadProjectVersion, expected); - const result = await determineEditorVersion(exampleProjectPath).promise; + const actual = await determineEditorVersion(exampleProjectPath); - expect(result).toBeOk((version) => expect(version).toEqual(expected)); - }); - - it("should fail if ProjectVersion.txt could not be loaded", async () => { - const { determineEditorVersion, loadProjectVersion } = makeDependencies(); - const expected = new GenericIOError("Read"); - loadProjectVersion.mockReturnValue(AsyncErr(expected)); - - const result = await determineEditorVersion(exampleProjectPath).promise; - - expect(result).toBeError((error) => expect(error).toEqual(expected)); + expect(actual).toEqual(expected); }); }); diff --git a/test/services/login.test.ts b/test/services/login.test.ts index 458da0d7..f8b92e0d 100644 --- a/test/services/login.test.ts +++ b/test/services/login.test.ts @@ -5,11 +5,6 @@ import { NpmLogin } from "../../src/services/npm-login"; import { AuthNpmrc } from "../../src/services/npmrc-auth"; import { exampleRegistryUrl } from "../domain/data-registry"; import { noopLogger } from "../../src/logging"; -import { - GenericIOError, - RegistryAuthenticationError, -} from "../../src/io/common-errors"; -import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; const exampleUser = "user"; const examplePassword = "pass"; @@ -21,13 +16,13 @@ const exampleToken = "some token"; describe("login", () => { function makeDependencies() { const saveAuthToUpmConfig = mockService(); - saveAuthToUpmConfig.mockReturnValue(AsyncOk()); + saveAuthToUpmConfig.mockResolvedValue(undefined); const npmLogin = mockService(); - npmLogin.mockReturnValue(AsyncOk(exampleToken)); + npmLogin.mockResolvedValue(exampleToken); const authNpmrc = mockService(); - authNpmrc.mockReturnValue(AsyncOk(exampleNpmrcPath)); + authNpmrc.mockResolvedValue(exampleNpmrcPath); const login = makeLogin( saveAuthToUpmConfig, @@ -50,7 +45,7 @@ describe("login", () => { exampleRegistryUrl, exampleConfigPath, "basic" - ).promise; + ); expect(saveAuthToUpmConfig).toHaveBeenCalledWith( exampleConfigPath, @@ -62,63 +57,9 @@ describe("login", () => { } ); }); - - it("should fail if config write fails", async () => { - const expected = new GenericIOError("Write"); - const { login, saveAuthToUpmConfig } = makeDependencies(); - saveAuthToUpmConfig.mockReturnValue(AsyncErr(expected)); - - const result = await login( - exampleUser, - examplePassword, - exampleEmail, - true, - exampleRegistryUrl, - exampleConfigPath, - "basic" - ).promise; - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); - }); }); describe("token auth", () => { - it("should fail if npm login fails", async () => { - const expected = new RegistryAuthenticationError(); - const { login, npmLogin } = makeDependencies(); - npmLogin.mockReturnValue(AsyncErr(expected)); - - const result = await login( - exampleUser, - examplePassword, - exampleEmail, - true, - exampleRegistryUrl, - exampleConfigPath, - "token" - ).promise; - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); - }); - - it("should fail if npmrc auth fails", async () => { - const expected = new GenericIOError("Read"); - const { login, authNpmrc } = makeDependencies(); - authNpmrc.mockReturnValue(AsyncErr(expected)); - - const result = await login( - exampleUser, - examplePassword, - exampleEmail, - true, - exampleRegistryUrl, - exampleConfigPath, - "token" - ).promise; - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); - }); - it("should save token", async () => { const { login, saveAuthToUpmConfig } = makeDependencies(); @@ -130,7 +71,7 @@ describe("login", () => { exampleRegistryUrl, exampleConfigPath, "token" - ).promise; + ); expect(saveAuthToUpmConfig).toHaveBeenCalledWith( exampleConfigPath, @@ -142,23 +83,5 @@ describe("login", () => { } ); }); - - it("should fail if config write fails", async () => { - const expected = new GenericIOError("Write"); - const { login, saveAuthToUpmConfig } = makeDependencies(); - saveAuthToUpmConfig.mockReturnValue(AsyncErr(expected)); - - const result = await login( - exampleUser, - examplePassword, - exampleEmail, - true, - exampleRegistryUrl, - exampleConfigPath, - "token" - ).promise; - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); - }); }); }); diff --git a/test/services/npm-login.test.ts b/test/services/npm-login.test.ts index 4b9c11dd..1e3cd360 100644 --- a/test/services/npm-login.test.ts +++ b/test/services/npm-login.test.ts @@ -31,14 +31,14 @@ describe("npm-login service", () => { null ); - const result = await addUser( + const actual = await addUser( exampleRegistryUrl, "valid-user", "valid@user.com", "valid-password" - ).promise; + ); - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); + expect(actual).toEqual(expected); }); it("should fail for not-ok response", async () => { @@ -55,16 +55,9 @@ describe("npm-login service", () => { } ); - const result = await addUser( - exampleRegistryUrl, - "bad-user", - "bad@user.com", - "bad-password" - ).promise; - - expect(result).toBeError((error) => - expect(error).toBeInstanceOf(RegistryAuthenticationError) - ); + await expect( + addUser(exampleRegistryUrl, "bad-user", "bad@user.com", "bad-password") + ).rejects.toBeInstanceOf(RegistryAuthenticationError); }); it("should fail for error response", async () => { @@ -79,15 +72,8 @@ describe("npm-login service", () => { } ); - const result = await addUser( - exampleRegistryUrl, - "bad-user", - "bad@user.com", - "bad-password" - ).promise; - - expect(result).toBeError((error) => - expect(error).toBeInstanceOf(RegistryAuthenticationError) - ); + await expect( + addUser(exampleRegistryUrl, "bad-user", "bad@user.com", "bad-password") + ).rejects.toBeInstanceOf(RegistryAuthenticationError); }); }); diff --git a/test/services/npmrc-auth.test.ts b/test/services/npmrc-auth.test.ts index 5a6af48a..67c81ccc 100644 --- a/test/services/npmrc-auth.test.ts +++ b/test/services/npmrc-auth.test.ts @@ -1,92 +1,51 @@ import { FindNpmrcPath, LoadNpmrc, SaveNpmrc } from "../../src/io/npmrc-io"; -import { AsyncResult, Err, Ok } from "ts-results-es"; -import { RequiredEnvMissingError } from "../../src/io/upm-config-io"; import { emptyNpmrc, setToken } from "../../src/domain/npmrc"; import { exampleRegistryUrl } from "../domain/data-registry"; import { makeAuthNpmrc } from "../../src/services/npmrc-auth"; import { mockService } from "./service.mock"; -import { GenericIOError } from "../../src/io/common-errors"; const exampleNpmrcPath = "/users/someuser/.npmrc"; function makeDependencies() { const findPath = mockService(); - findPath.mockReturnValue(Ok(exampleNpmrcPath)); + findPath.mockReturnValue(exampleNpmrcPath); const loadNpmrc = mockService(); - loadNpmrc.mockReturnValue(new AsyncResult(Ok(null))); + loadNpmrc.mockResolvedValue(null); const saveNpmrc = mockService(); - saveNpmrc.mockReturnValue(new AsyncResult(Ok(undefined))); + saveNpmrc.mockResolvedValue(undefined); const authNpmrc = makeAuthNpmrc(findPath, loadNpmrc, saveNpmrc); return { authNpmrc, findPath, loadNpmrc, saveNpmrc } as const; } describe("npmrc-auth", () => { - describe("set token", () => { - it("should fail if path could not be determined", async () => { - const expected = new RequiredEnvMissingError([]); - const { authNpmrc, findPath } = makeDependencies(); - findPath.mockReturnValue(Err(expected)); + it("should return npmrc path", async () => { + const { authNpmrc } = makeDependencies(); - const result = await authNpmrc(exampleRegistryUrl, "some token").promise; + const actual = await authNpmrc(exampleRegistryUrl, "some token"); - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); - }); - - it("should fail if npmrc load failed", async () => { - const expected = new GenericIOError("Read"); - const { authNpmrc, loadNpmrc } = makeDependencies(); - loadNpmrc.mockReturnValue(new AsyncResult(Err(expected))); - - const result = await authNpmrc(exampleRegistryUrl, "some token").promise; - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); - }); - - it("should fail if npmrc save failed", async () => { - const expected = new GenericIOError("Write"); - const { authNpmrc, saveNpmrc } = makeDependencies(); - saveNpmrc.mockReturnValue(new AsyncResult(Err(expected))); - - const result = await authNpmrc(exampleRegistryUrl, "some token").promise; - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); - }); - - it("should return npmrc path", async () => { - const { authNpmrc } = makeDependencies(); - - const result = await authNpmrc(exampleRegistryUrl, "some token").promise; - - expect(result).toBeOk((actual) => - expect(actual).toEqual(exampleNpmrcPath) - ); - }); + expect(actual).toEqual(exampleNpmrcPath); + }); - it("should create npmrc if missing", async () => { - const expected = setToken(emptyNpmrc, exampleRegistryUrl, "some token"); - const { authNpmrc, saveNpmrc } = makeDependencies(); + it("should create npmrc if missing", async () => { + const expected = setToken(emptyNpmrc, exampleRegistryUrl, "some token"); + const { authNpmrc, saveNpmrc } = makeDependencies(); - await authNpmrc(exampleRegistryUrl, "some token").promise; + await authNpmrc(exampleRegistryUrl, "some token"); - expect(saveNpmrc).toHaveBeenCalledWith(exampleNpmrcPath, expected); - }); + expect(saveNpmrc).toHaveBeenCalledWith(exampleNpmrcPath, expected); + }); - it("should update npmrc if already exists", async () => { - const { authNpmrc, loadNpmrc, saveNpmrc } = makeDependencies(); - const initial = setToken( - emptyNpmrc, - exampleRegistryUrl, - "some old token" - ); - const expected = setToken(initial, exampleRegistryUrl, "some token"); - loadNpmrc.mockReturnValue(new AsyncResult(Ok(initial))); + it("should update npmrc if already exists", async () => { + const { authNpmrc, loadNpmrc, saveNpmrc } = makeDependencies(); + const initial = setToken(emptyNpmrc, exampleRegistryUrl, "some old token"); + const expected = setToken(initial, exampleRegistryUrl, "some token"); + loadNpmrc.mockResolvedValue(initial); - await authNpmrc(exampleRegistryUrl, "some token").promise; + await authNpmrc(exampleRegistryUrl, "some token"); - expect(saveNpmrc).toHaveBeenCalledWith(exampleNpmrcPath, expected); - }); + expect(saveNpmrc).toHaveBeenCalledWith(exampleNpmrcPath, expected); }); }); diff --git a/test/services/packument-resolving.mock.ts b/test/services/packument-resolving.mock.ts index b8e1d498..51973662 100644 --- a/test/services/packument-resolving.mock.ts +++ b/test/services/packument-resolving.mock.ts @@ -1,4 +1,3 @@ -import { Err } from "ts-results-es"; import { PackumentNotFoundError } from "../../src/common-errors"; import { tryResolvePackumentVersion, @@ -6,6 +5,7 @@ import { } from "../../src/domain/packument"; import { RegistryUrl } from "../../src/domain/registry-url"; import { ResolveRemotePackumentVersion } from "../../src/services/resolve-remote-packument-version"; +import { AsyncErr } from "../../src/utils/result-utils"; type MockEntry = [RegistryUrl, UnityPackument]; @@ -26,7 +26,7 @@ export function mockResolvedPackuments( (entry) => entry[0] === registry.url && entry[1].name === name ); if (matchingEntry === undefined) - return Err(new PackumentNotFoundError(name)).toAsyncResult(); + return AsyncErr(new PackumentNotFoundError(name)); const source = matchingEntry[0]; const packument = matchingEntry[1]; diff --git a/test/services/parse-env.test.ts b/test/services/parse-env.test.ts index 3bf3b935..c6efee69 100644 --- a/test/services/parse-env.test.ts +++ b/test/services/parse-env.test.ts @@ -1,15 +1,13 @@ import { TokenAuth, UPMConfig } from "../../src/domain/upm-config"; import { NpmAuth } from "another-npm-registry-client"; -import { Env, makeParseEnv } from "../../src/services/parse-env"; +import { makeParseEnv } from "../../src/services/parse-env"; import { GetUpmConfigPath, LoadUpmConfig } from "../../src/io/upm-config-io"; -import { NoWslError } from "../../src/io/wsl"; -import { mockUpmConfig } from "../io/upm-config-io.mock"; import { exampleRegistryUrl } from "../domain/data-registry"; import { makeMockLogger } from "../cli/log.mock"; import { mockService } from "./service.mock"; import { GetCwd } from "../../src/io/special-paths"; import path from "path"; -import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; +import { noopLogger } from "../../src/logging"; const testRootPath = "/users/some-user/projects/MyUnityProject"; @@ -32,16 +30,22 @@ function makeDependencies() { const getUpmConfigPath = mockService(); // The root directory does not contain an upm-config - getUpmConfigPath.mockReturnValue(AsyncOk(testRootPath)); + getUpmConfigPath.mockResolvedValue(testRootPath); const loadUpmConfig = mockService(); - mockUpmConfig(loadUpmConfig, null); + loadUpmConfig.mockResolvedValue(null); // process.cwd is in the root directory. const getCwd = mockService(); getCwd.mockReturnValue(testRootPath); - const parseEnv = makeParseEnv(log, getUpmConfigPath, loadUpmConfig, getCwd); + const parseEnv = makeParseEnv( + log, + getUpmConfigPath, + loadUpmConfig, + getCwd, + noopLogger + ); return { parseEnv, log, @@ -133,35 +137,35 @@ describe("env", () => { it("should use upstream if upstream option is true", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ + const env = await parseEnv({ _global: { upstream: true, }, }); - expect(result).toBeOk((env: Env) => expect(env.upstream).toBeTruthy()); + expect(env.upstream).toBeTruthy(); }); it("should use upstream if upstream option is missing", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ + const env = await parseEnv({ _global: {}, }); - expect(result).toBeOk((env: Env) => expect(env.upstream).toBeTruthy()); + expect(env.upstream).toBeTruthy(); }); it("should not use upstream if upstream option is false", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ + const env = await parseEnv({ _global: { upstream: false, }, }); - expect(result).toBeOk((env: Env) => expect(env.upstream).toBeFalsy()); + expect(env.upstream).toBeFalsy(); }); }); @@ -169,35 +173,35 @@ describe("env", () => { it("should be system-user if option is true", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ + const env = await parseEnv({ _global: { systemUser: true, }, }); - expect(result).toBeOk((env: Env) => expect(env.systemUser).toBeTruthy()); + expect(env.systemUser).toBeTruthy(); }); it("should not be system-user if option is missing", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ + const env = await parseEnv({ _global: {}, }); - expect(result).toBeOk((env: Env) => expect(env.systemUser).toBeFalsy()); + expect(env.systemUser).toBeFalsy(); }); it("should not be system-user if option is false", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ + const env = await parseEnv({ _global: { systemUser: false, }, }); - expect(result).toBeOk((env: Env) => expect(env.systemUser).toBeFalsy()); + expect(env.systemUser).toBeFalsy(); }); }); @@ -205,47 +209,35 @@ describe("env", () => { it("should use wsl if option is true", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ + const env = await parseEnv({ _global: { wsl: true, }, }); - expect(result).toBeOk((env: Env) => expect(env.wsl).toBeTruthy()); + expect(env.wsl).toBeTruthy(); }); it("should not use wsl if option is missing", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ + const env = await parseEnv({ _global: {}, }); - expect(result).toBeOk((env: Env) => expect(env.wsl).toBeFalsy()); + expect(env.wsl).toBeFalsy(); }); it("should not use wsl if option is false", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ + const env = await parseEnv({ _global: { wsl: false, }, }); - expect(result).toBeOk((env: Env) => expect(env.wsl).toBeFalsy()); - }); - }); - - describe("upm-config", () => { - it("should fail if upm-config dir cannot be determined", async () => { - const { parseEnv, getUpmConfigPath } = makeDependencies(); - const expected = new NoWslError(); - getUpmConfigPath.mockReturnValue(AsyncErr(expected)); - - const result = await parseEnv({ _global: {} }); - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); + expect(env.wsl).toBeFalsy(); }); }); @@ -253,61 +245,53 @@ describe("env", () => { it("should be global openupm by default", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ _global: {} }); + const env = await parseEnv({ _global: {} }); - expect(result).toBeOk((env: Env) => - expect(env.registry.url).toEqual("https://package.openupm.com") - ); + expect(env.registry.url).toEqual("https://package.openupm.com"); }); it("should be custom registry if overridden", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ + const env = await parseEnv({ _global: { registry: exampleRegistryUrl, }, }); - expect(result).toBeOk((env: Env) => - expect(env.registry.url).toEqual(exampleRegistryUrl) - ); + expect(env.registry.url).toEqual(exampleRegistryUrl); }); it("should have no auth if no upm-config was found", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ + const env = await parseEnv({ _global: { registry: exampleRegistryUrl, }, }); - expect(result).toBeOk((env: Env) => - expect(env.registry.auth).toEqual(null) - ); + expect(env.registry.auth).toEqual(null); }); it("should have no auth if upm-config had no entry for the url", async () => { const { parseEnv, loadUpmConfig } = makeDependencies(); - mockUpmConfig(loadUpmConfig, { + loadUpmConfig.mockResolvedValue({ npmAuth: {}, }); - const result = await parseEnv({ + const env = await parseEnv({ _global: { registry: exampleRegistryUrl, }, }); - expect(result).toBeOk((env: Env) => - expect(env.registry.auth).toEqual(null) - ); + expect(env.registry.auth).toEqual(null); }); it("should notify if upm-config did not have auth", async () => { const { parseEnv, log, loadUpmConfig } = makeDependencies(); - mockUpmConfig(loadUpmConfig, { + loadUpmConfig.mockResolvedValue({ npmAuth: {}, }); @@ -325,17 +309,15 @@ describe("env", () => { it("should have auth if upm-config had entry for the url", async () => { const { parseEnv, loadUpmConfig } = makeDependencies(); - mockUpmConfig(loadUpmConfig, testUpmConfig); + loadUpmConfig.mockResolvedValue(testUpmConfig); - const result = await parseEnv({ + const env = await parseEnv({ _global: { registry: exampleRegistryUrl, }, }); - expect(result).toBeOk((env: Env) => - expect(env.registry.auth).toEqual(testNpmAuth) - ); + expect(env.registry.auth).toEqual(testNpmAuth); }); }); @@ -343,23 +325,19 @@ describe("env", () => { it("should be global unity by default", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ _global: {} }); + const env = await parseEnv({ _global: {} }); - expect(result).toBeOk((env: Env) => - expect(env.upstreamRegistry.url).toEqual("https://packages.unity.com") - ); + expect(env.upstreamRegistry.url).toEqual("https://packages.unity.com"); }); it("should have no auth", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ + const env = await parseEnv({ _global: {}, }); - expect(result).toBeOk((env: Env) => - expect(env.upstreamRegistry.auth).toEqual(null) - ); + expect(env.upstreamRegistry.auth).toEqual(null); }); }); @@ -367,13 +345,11 @@ describe("env", () => { it("should be process directory by default", async () => { const { parseEnv } = makeDependencies(); - const result = await parseEnv({ + const env = await parseEnv({ _global: {}, }); - expect(result).toBeOk((env: Env) => - expect(env.cwd).toEqual(testRootPath) - ); + expect(env.cwd).toEqual(testRootPath); }); it("should be specified path if overridden", async () => { @@ -381,13 +357,13 @@ describe("env", () => { const expected = path.resolve("/some/other/path"); - const result = await parseEnv({ + const env = await parseEnv({ _global: { chdir: expected, }, }); - expect(result).toBeOk((env: Env) => expect(env.cwd).toEqual(expected)); + expect(env.cwd).toEqual(expected); }); }); }); diff --git a/test/services/remove-packages.test.ts b/test/services/remove-packages.test.ts index 459941ab..3fb8beec 100644 --- a/test/services/remove-packages.test.ts +++ b/test/services/remove-packages.test.ts @@ -1,8 +1,3 @@ -import { FileMissingError, GenericIOError } from "../../src/io/common-errors"; -import { - mockProjectManifest, - mockProjectManifestWriteResult, -} from "../io/project-manifest-io.mock"; import { makeRemovePackages, RemovedPackage, @@ -28,10 +23,10 @@ describe("remove packages", () => { function makeDependencies() { const loadProjectManifest = mockService(); - mockProjectManifest(loadProjectManifest, defaultManifest); + loadProjectManifest.mockResolvedValue(defaultManifest); const writeProjectManifest = mockService(); - mockProjectManifestWriteResult(writeProjectManifest); + writeProjectManifest.mockResolvedValue(undefined); const removePackages = makeRemovePackages( loadProjectManifest, @@ -44,17 +39,6 @@ describe("remove packages", () => { } as const; } - it("should fail if manifest could not be loaded", async () => { - const { removePackages, loadProjectManifest } = makeDependencies(); - mockProjectManifest(loadProjectManifest, null); - - const result = await removePackages(someProjectPath, [somePackage]).promise; - - expect(result).toBeError((actual) => - expect(actual).toBeInstanceOf(FileMissingError) - ); - }); - it("should fail if package is not in manifest", async () => { const { removePackages } = makeDependencies(); @@ -112,14 +96,4 @@ describe("remove packages", () => { }) ); }); - - it("should fail if manifest could not be saved", async () => { - const expected = new GenericIOError("Write"); - const { removePackages, writeProjectManifest } = makeDependencies(); - mockProjectManifestWriteResult(writeProjectManifest, expected); - - const result = await removePackages(someProjectPath, [somePackage]).promise; - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); - }); }); diff --git a/test/services/resolve-latest-version.test.ts b/test/services/resolve-latest-version.test.ts index 7f51d277..38bd2967 100644 --- a/test/services/resolve-latest-version.test.ts +++ b/test/services/resolve-latest-version.test.ts @@ -1,13 +1,11 @@ import { mockService } from "./service.mock"; import { makeResolveLatestVersion } from "../../src/services/resolve-latest-version"; import { makeDomainName } from "../../src/domain/domain-name"; -import { PackumentNotFoundError } from "../../src/common-errors"; -import { NoVersionsError, UnityPackument } from "../../src/domain/packument"; +import { UnityPackument } from "../../src/domain/packument"; import { Registry } from "../../src/domain/registry"; import { exampleRegistryUrl } from "../domain/data-registry"; import { unityRegistryUrl } from "../../src/domain/registry-url"; import { FetchPackument } from "../../src/io/packument-io"; -import { AsyncOk } from "../../src/utils/result-utils"; import { SemanticVersion } from "../../src/domain/semantic-version"; describe("resolve latest version service", () => { @@ -23,14 +21,12 @@ describe("resolve latest version service", () => { return { resolveLatestVersion, fetchPackument } as const; } - it("should fail if not given any sources", async () => { + it("should be null if not given any sources", async () => { const { resolveLatestVersion } = makeDependencies(); - const result = await resolveLatestVersion([], somePackage).promise; + const actual = await resolveLatestVersion([], somePackage); - expect(result).toBeError((error) => - expect(error).toBeInstanceOf(PackumentNotFoundError) - ); + expect(actual).toBeNull(); }); it("should get specified latest version from first packument", async () => { @@ -39,14 +35,11 @@ describe("resolve latest version service", () => { versions: { ["1.0.0" as SemanticVersion]: { version: "1.0.0" } }, "dist-tags": { latest: "1.0.0" }, } as UnityPackument; - fetchPackument.mockReturnValue(AsyncOk(packument)); + fetchPackument.mockResolvedValue(packument); - const result = await resolveLatestVersion([exampleRegistry], somePackage) - .promise; + const actual = await resolveLatestVersion([exampleRegistry], somePackage); - expect(result).toBeOk((value) => - expect(value).toEqual({ value: "1.0.0", source: exampleRegistry.url }) - ); + expect(actual).toEqual({ value: "1.0.0", source: exampleRegistry.url }); }); it("should get latest listed version from first packument if no latest was specified", async () => { @@ -54,27 +47,11 @@ describe("resolve latest version service", () => { const packument = { versions: { ["1.0.0" as SemanticVersion]: { version: "1.0.0" } }, } as UnityPackument; - fetchPackument.mockReturnValue(AsyncOk(packument)); + fetchPackument.mockResolvedValue(packument); - const result = await resolveLatestVersion([exampleRegistry], somePackage) - .promise; + const actual = await resolveLatestVersion([exampleRegistry], somePackage); - expect(result).toBeOk((value) => - expect(value).toEqual({ value: "1.0.0", source: exampleRegistry.url }) - ); - }); - - it("should fail if packument had no versions", async () => { - const { resolveLatestVersion, fetchPackument } = makeDependencies(); - const packument = { versions: {} } as UnityPackument; - fetchPackument.mockReturnValue(AsyncOk(packument)); - - const result = await resolveLatestVersion([exampleRegistry], somePackage) - .promise; - - expect(result).toBeError((error) => - expect(error).toBeInstanceOf(NoVersionsError) - ); + expect(actual).toEqual({ value: "1.0.0", source: exampleRegistry.url }); }); it("should check all registries", async () => { @@ -83,16 +60,14 @@ describe("resolve latest version service", () => { versions: { ["1.0.0" as SemanticVersion]: { version: "1.0.0" } }, "dist-tags": { latest: "1.0.0" }, } as UnityPackument; - fetchPackument.mockReturnValueOnce(AsyncOk(null)); - fetchPackument.mockReturnValueOnce(AsyncOk(packument)); + fetchPackument.mockResolvedValueOnce(null); + fetchPackument.mockResolvedValueOnce(packument); - const result = await resolveLatestVersion( + const actual = await resolveLatestVersion( [exampleRegistry, upstreamRegistry], somePackage - ).promise; - - expect(result).toBeOk((value) => - expect(value).toEqual({ value: "1.0.0", source: upstreamRegistry.url }) ); + + expect(actual).toEqual({ value: "1.0.0", source: upstreamRegistry.url }); }); }); diff --git a/test/services/resolve-remote-packument-version.test.ts b/test/services/resolve-remote-packument-version.test.ts index 43686933..0dc26d7d 100644 --- a/test/services/resolve-remote-packument-version.test.ts +++ b/test/services/resolve-remote-packument-version.test.ts @@ -7,7 +7,6 @@ import { exampleRegistryUrl } from "../domain/data-registry"; import { PackumentNotFoundError } from "../../src/common-errors"; import { buildPackument } from "../domain/data-packument"; import { FetchPackument } from "../../src/io/packument-io"; -import { AsyncOk } from "../../src/utils/result-utils"; describe("resolve remote packument version", () => { const somePackage = makeDomainName("com.some.package"); @@ -27,7 +26,7 @@ describe("resolve remote packument version", () => { it("should fail if packument was not found", async () => { const { resolveRemovePackumentVersion, fetchPackument } = makeDependencies(); - fetchPackument.mockReturnValue(AsyncOk(null)); + fetchPackument.mockResolvedValue(null); const result = await resolveRemovePackumentVersion( somePackage, @@ -48,7 +47,7 @@ describe("resolve remote packument version", () => { version.addDependency("com.other.package", "1.0.0") ) ); - fetchPackument.mockReturnValue(AsyncOk(packument)); + fetchPackument.mockResolvedValue(packument); const result = await resolveRemovePackumentVersion( somePackage, diff --git a/test/services/search-packages.test.ts b/test/services/search-packages.test.ts index 4de7be93..59c84ec2 100644 --- a/test/services/search-packages.test.ts +++ b/test/services/search-packages.test.ts @@ -7,11 +7,9 @@ import { import { makeSearchPackages } from "../../src/services/search-packages"; import { Registry } from "../../src/domain/registry"; import { exampleRegistryUrl } from "../domain/data-registry"; -import { Err } from "ts-results-es"; import { makeDomainName } from "../../src/domain/domain-name"; import { makeSemanticVersion } from "../../src/domain/semantic-version"; -import { GenericNetworkError } from "../../src/io/common-errors"; -import { AsyncOk } from "../../src/utils/result-utils"; +import { noopLogger } from "../../src/logging"; describe("search packages", () => { const exampleRegistry: Registry = { @@ -36,14 +34,15 @@ describe("search packages", () => { function makeDependencies() { const searchRegistry = mockService(); - searchRegistry.mockReturnValue(AsyncOk([exampleSearchResult])); + searchRegistry.mockResolvedValue([exampleSearchResult]); const fetchAllPackument = mockService(); - fetchAllPackument.mockReturnValue(AsyncOk(exampleAllPackumentsResult)); + fetchAllPackument.mockResolvedValue(exampleAllPackumentsResult); const searchPackages = makeSearchPackages( searchRegistry, - fetchAllPackument + fetchAllPackument, + noopLogger ); return { searchPackages, searchRegistry, fetchAllPackument } as const; } @@ -51,74 +50,45 @@ describe("search packages", () => { it("should search using search api first", async () => { const { searchPackages } = makeDependencies(); - const result = await searchPackages(exampleRegistry, exampleKeyword) - .promise; + const actual = await searchPackages(exampleRegistry, exampleKeyword); - expect(result).toBeOk((actual) => - expect(actual).toEqual([exampleSearchResult]) - ); + expect(actual).toEqual([exampleSearchResult]); }); it("should not notify of fallback when using search api", async () => { const { searchPackages } = makeDependencies(); const fallback = jest.fn(); - await searchPackages(exampleRegistry, exampleKeyword, fallback).promise; + await searchPackages(exampleRegistry, exampleKeyword, fallback); expect(fallback).not.toHaveBeenCalled(); }); it("should search using old search if search api is not available", async () => { const { searchPackages, searchRegistry } = makeDependencies(); - searchRegistry.mockReturnValue( - Err(new GenericNetworkError()).toAsyncResult() - ); + searchRegistry.mockRejectedValue(new Error()); - const result = await searchPackages(exampleRegistry, exampleKeyword) - .promise; + const actual = await searchPackages(exampleRegistry, exampleKeyword); - expect(result).toBeOk((actual) => - expect(actual).toEqual([exampleSearchResult]) - ); + expect(actual).toEqual([exampleSearchResult]); }); it("should notify of using old search", async () => { const { searchPackages, searchRegistry } = makeDependencies(); - searchRegistry.mockReturnValue( - Err(new GenericNetworkError()).toAsyncResult() - ); + searchRegistry.mockRejectedValue(new Error()); const fallback = jest.fn(); - await searchPackages(exampleRegistry, exampleKeyword, fallback).promise; + await searchPackages(exampleRegistry, exampleKeyword, fallback); expect(fallback).toHaveBeenCalled(); }); it("should not find packages not matching the keyword using old search", async () => { const { searchPackages, searchRegistry } = makeDependencies(); - searchRegistry.mockReturnValue( - Err(new GenericNetworkError()).toAsyncResult() - ); - - const result = await searchPackages(exampleRegistry, "some other keyword") - .promise; - - expect(result).toBeOk((actual) => expect(actual).toEqual([])); - }); - - it("should fail if both search strategies fail", async () => { - const { searchPackages, searchRegistry, fetchAllPackument } = - makeDependencies(); - searchRegistry.mockReturnValue( - Err(new GenericNetworkError()).toAsyncResult() - ); - fetchAllPackument.mockReturnValue( - Err(new GenericNetworkError()).toAsyncResult() - ); + searchRegistry.mockRejectedValue(new Error()); - const result = await searchPackages(exampleRegistry, exampleKeyword) - .promise; + const actual = await searchPackages(exampleRegistry, "some other keyword"); - expect(result).toBeError(); + expect(actual).toEqual([]); }); }); diff --git a/test/services/unity-package-check.test.ts b/test/services/unity-package-check.test.ts index e902b1fb..9d064edc 100644 --- a/test/services/unity-package-check.test.ts +++ b/test/services/unity-package-check.test.ts @@ -1,9 +1,7 @@ import { makeCheckIsUnityPackage } from "../../src/services/unity-package-check"; import { mockService } from "./service.mock"; import { CheckUrlExists } from "../../src/io/check-url"; -import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; import { makeDomainName } from "../../src/domain/domain-name"; -import { GenericNetworkError } from "../../src/io/common-errors"; describe("is unity package", () => { const somePackage = makeDomainName("com.some.package"); @@ -17,29 +15,19 @@ describe("is unity package", () => { it("should be true if manual page exists", async () => { const { checkIsUnityPackage, checkUrlExists } = makeDependencies(); - checkUrlExists.mockReturnValue(AsyncOk(true)); + checkUrlExists.mockResolvedValue(true); - const result = await checkIsUnityPackage(somePackage).promise; + const actual = await checkIsUnityPackage(somePackage); - expect(result).toBeOk((actual) => expect(actual).toBeTruthy()); + expect(actual).toBeTruthy(); }); it("should be false if manual page does not exist", async () => { const { checkIsUnityPackage, checkUrlExists } = makeDependencies(); - checkUrlExists.mockReturnValue(AsyncOk(false)); + checkUrlExists.mockResolvedValue(false); - const result = await checkIsUnityPackage(somePackage).promise; + const actual = await checkIsUnityPackage(somePackage); - expect(result).toBeOk((actual) => expect(actual).toBeFalsy()); - }); - - it("should fail if page status could not be verified", async () => { - const expected = new GenericNetworkError(); - const { checkIsUnityPackage, checkUrlExists } = makeDependencies(); - checkUrlExists.mockReturnValue(AsyncErr(expected)); - - const result = await checkIsUnityPackage(somePackage).promise; - - expect(result).toBeError((actual) => expect(actual).toEqual(expected)); + expect(actual).toBeFalsy(); }); }); diff --git a/test/utils/sources.test.ts b/test/utils/sources.test.ts index 0016fe8e..3a02f93d 100644 --- a/test/utils/sources.test.ts +++ b/test/utils/sources.test.ts @@ -1,5 +1,4 @@ import { queryAllRegistriesLazy } from "../../src/utils/sources"; -import { AsyncErr, AsyncOk } from "../../src/utils/result-utils"; import { Registry } from "../../src/domain/registry"; import { exampleRegistryUrl } from "../domain/data-registry"; import { unityRegistryUrl } from "../../src/domain/registry-url"; @@ -13,31 +12,27 @@ describe("sources", () => { it("should return null if not given any sources", async () => { const sources = Array.of(); - const result = await queryAllRegistriesLazy(sources, () => AsyncOk(1)) - .promise; + const actual = await queryAllRegistriesLazy(sources, async () => 1); - expect(result).toBeOk((actual) => expect(actual).toBeNull()); + expect(actual).toBeNull(); }); it("should return null if no source matched the query", async () => { const sources = [exampleRegistryA, exampleRegistryB]; - const result = await queryAllRegistriesLazy(sources, () => AsyncOk(null)) - .promise; + const actual = await queryAllRegistriesLazy(sources, async () => null); - expect(result).toBeOk((actual) => expect(actual).toBeNull()); + expect(actual).toBeNull(); }); - it("should return the first encountered error", async () => { + it("should throw first encountered error", async () => { const sources = [exampleRegistryA, exampleRegistryB]; - const result = await queryAllRegistriesLazy(sources, (source) => - AsyncErr({ source: source.url }) - ).promise; - - expect(result).toBeError((actual) => - expect(actual).toEqual({ source: exampleRegistryA.url }) - ); + await expect( + queryAllRegistriesLazy(sources, (source) => { + throw { source: source.url }; + }) + ).rejects.toEqual({ source: exampleRegistryA.url }); }); it("should stop query when encountering an error", async () => { @@ -46,8 +41,10 @@ describe("sources", () => { await queryAllRegistriesLazy(sources, (source) => { fn(source.url); - return AsyncErr({ source: source.url }); - }).promise; + throw new Error(); + }) + // Empty catch so thrown error does not trigger a failed test + .catch(() => {}); expect(fn).not.toHaveBeenCalledWith(exampleRegistryB.url); }); @@ -55,24 +52,19 @@ describe("sources", () => { it("should return value from first registry if there is a match", async () => { const sources = [exampleRegistryA, exampleRegistryB]; - const result = await queryAllRegistriesLazy(sources, () => AsyncOk(1)) - .promise; + const actual = await queryAllRegistriesLazy(sources, async () => 1); - expect(result).toBeOk((actual) => - expect(actual).toEqual({ value: 1, source: exampleRegistryA.url }) - ); + expect(actual).toEqual({ value: 1, source: exampleRegistryA.url }); }); it("should return value from fallback registry if there is no match in the first", async () => { const sources = [exampleRegistryA, exampleRegistryB]; - const result = await queryAllRegistriesLazy(sources, (source) => - AsyncOk(source.url === exampleRegistryB.url ? 1 : null) - ).promise; - - expect(result).toBeOk((actual) => - expect(actual).toEqual({ value: 1, source: exampleRegistryB.url }) + const actual = await queryAllRegistriesLazy(sources, async (source) => + source.url === exampleRegistryB.url ? 1 : null ); + + expect(actual).toEqual({ value: 1, source: exampleRegistryB.url }); }); }); }); diff --git a/test/utils/string-parsing.test.ts b/test/utils/string-parsing.test.ts deleted file mode 100644 index a7c5421d..00000000 --- a/test/utils/string-parsing.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - StringFormatError, - tryParseJson, - tryParseToml, - tryParseYaml, -} from "../../src/utils/string-parsing"; -import yaml from "yaml"; -import TOML from "@iarna/toml"; - -describe("string-parsing", () => { - describe("json", () => { - it("should round-trip valid json", () => { - const expected = { test: 123 }; - const json = JSON.stringify(expected); - - const result = tryParseJson(json); - - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); - }); - - it("should fail for bad data", () => { - const json = "{ invalid json "; - - const result = tryParseJson(json); - - expect(result).toBeError((error) => - expect(error).toBeInstanceOf(StringFormatError) - ); - }); - }); - - describe("toml", () => { - it("should round-trip toml", () => { - const expected = { test: 123 }; - const toml = TOML.stringify(expected); - - const result = tryParseToml(toml); - - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); - }); - - it("should fail for bad data", () => { - const toml = "{ invalid toml "; - - const result = tryParseToml(toml); - - expect(result).toBeError((error) => - expect(error).toBeInstanceOf(StringFormatError) - ); - }); - }); - - describe("yaml", () => { - it("should round-trip valid yaml", () => { - const expected = { test: 123 }; - const text = yaml.stringify(expected); - - const result = tryParseYaml(text); - - expect(result).toBeOk((actual) => expect(actual).toEqual(expected)); - }); - - it("should fail for bad yaml", () => { - const yaml = "{ invalid yaml "; - - const result = tryParseYaml(yaml); - - expect(result).toBeError((error) => - expect(error).toBeInstanceOf(StringFormatError) - ); - }); - }); -});