diff --git a/apps/server/src/modules/tool/common/controller/dto/context-external-tool-configuration-status.response.ts b/apps/server/src/modules/tool/common/controller/dto/context-external-tool-configuration-status.response.ts index 8a4fa696b23..96b5adf31c3 100644 --- a/apps/server/src/modules/tool/common/controller/dto/context-external-tool-configuration-status.response.ts +++ b/apps/server/src/modules/tool/common/controller/dto/context-external-tool-configuration-status.response.ts @@ -17,10 +17,17 @@ export class ContextExternalToolConfigurationStatusResponse { @ApiProperty({ type: Boolean, - description: 'True, if a configured parameter on the context external tool is missing a value', + description: 'True, if a mandatory parameter on the context external tool is missing a value', }) isIncompleteOnScopeContext: boolean; + @ApiProperty({ + type: Boolean, + description: + 'True, if a optional parameter on the context external tool is missing a value. This is happening, when course is copied.', + }) + isIncompleteOperationalOnScopeContext: boolean; + @ApiProperty({ type: Boolean, description: 'Is the tool deactivated, because of superhero or school administrator', @@ -31,6 +38,7 @@ export class ContextExternalToolConfigurationStatusResponse { this.isOutdatedOnScopeSchool = props.isOutdatedOnScopeSchool; this.isOutdatedOnScopeContext = props.isOutdatedOnScopeContext; this.isIncompleteOnScopeContext = props.isIncompleteOnScopeContext; + this.isIncompleteOperationalOnScopeContext = props.isIncompleteOperationalOnScopeContext; this.isDeactivated = props.isDeactivated; } } diff --git a/apps/server/src/modules/tool/common/domain/context-external-tool-configuration-status.ts b/apps/server/src/modules/tool/common/domain/context-external-tool-configuration-status.ts index 72daab380e8..b46506f0399 100644 --- a/apps/server/src/modules/tool/common/domain/context-external-tool-configuration-status.ts +++ b/apps/server/src/modules/tool/common/domain/context-external-tool-configuration-status.ts @@ -7,10 +7,13 @@ export class ContextExternalToolConfigurationStatus { isDeactivated: boolean; + isIncompleteOperationalOnScopeContext: boolean; + constructor(props: ContextExternalToolConfigurationStatus) { this.isOutdatedOnScopeSchool = props.isOutdatedOnScopeSchool; this.isOutdatedOnScopeContext = props.isOutdatedOnScopeContext; this.isIncompleteOnScopeContext = props.isIncompleteOnScopeContext; + this.isIncompleteOperationalOnScopeContext = props.isIncompleteOperationalOnScopeContext; this.isDeactivated = props.isDeactivated; } } diff --git a/apps/server/src/modules/tool/common/domain/error/index.ts b/apps/server/src/modules/tool/common/domain/error/index.ts index 22ea23bcfd9..d9913ba0510 100644 --- a/apps/server/src/modules/tool/common/domain/error/index.ts +++ b/apps/server/src/modules/tool/common/domain/error/index.ts @@ -3,5 +3,6 @@ export { ToolParameterRequiredLoggableException } from './tool-parameter-require export { ToolParameterUnknownLoggableException } from './tool-parameter-unknown.loggable-exception'; export { ToolParameterValueRegexLoggableException } from './tool-parameter-value-regex.loggable-exception'; export { ToolParameterTypeMismatchLoggableException } from './tool-parameter-type-mismatch.loggable-exception'; -export { ToolParameterValueMissingLoggableException } from './tool-parameter-value-missing.loggable-exception'; +export { ToolParameterMandatoryValueMissingLoggableException } from './tool-parameter-mandatory-value-missing-loggable.exception'; export { ContextExternalToolNameAlreadyExistsLoggableException } from './context-external-tool-name-already-exists.loggable-exception'; +export { ToolParameterOptionalValueMissingLoggableException } from './tool-parameter-optional-value-missing-loggable-exception'; diff --git a/apps/server/src/modules/tool/common/domain/error/tool-parameter-value-missing.loggable-exception.ts b/apps/server/src/modules/tool/common/domain/error/tool-parameter-mandatory-value-missing-loggable.exception.ts similarity index 74% rename from apps/server/src/modules/tool/common/domain/error/tool-parameter-value-missing.loggable-exception.ts rename to apps/server/src/modules/tool/common/domain/error/tool-parameter-mandatory-value-missing-loggable.exception.ts index b3569357fe0..901b0c9bc9a 100644 --- a/apps/server/src/modules/tool/common/domain/error/tool-parameter-value-missing.loggable-exception.ts +++ b/apps/server/src/modules/tool/common/domain/error/tool-parameter-mandatory-value-missing-loggable.exception.ts @@ -4,13 +4,13 @@ import { EntityId } from '@shared/domain/types'; import { HttpStatus } from '@nestjs/common'; import { CustomParameter } from '../custom-parameter.do'; -export class ToolParameterValueMissingLoggableException extends BusinessError implements Loggable { +export class ToolParameterMandatoryValueMissingLoggableException extends BusinessError implements Loggable { constructor(private readonly validatableToolId: EntityId | undefined, private readonly parameter: CustomParameter) { super( { - type: 'TOOL_PARAMETER_VALUE_MISSING', - title: 'Missing tool parameter value', - defaultMessage: 'The parameter has no value.', + type: 'TOOL_PARAMETER_MANDATORY_VALUE_MISSING', + title: 'Missing mandatory tool parameter value', + defaultMessage: 'The mandatory parameter has no value.', }, HttpStatus.BAD_REQUEST, { diff --git a/apps/server/src/modules/tool/common/domain/error/tool-parameter-value-missing.loggable-exception.spec.ts b/apps/server/src/modules/tool/common/domain/error/tool-parameter-mandatory-value-missing.loggable-exception.spec.ts similarity index 56% rename from apps/server/src/modules/tool/common/domain/error/tool-parameter-value-missing.loggable-exception.spec.ts rename to apps/server/src/modules/tool/common/domain/error/tool-parameter-mandatory-value-missing.loggable-exception.spec.ts index 44a0ac62e26..1dc12574766 100644 --- a/apps/server/src/modules/tool/common/domain/error/tool-parameter-value-missing.loggable-exception.spec.ts +++ b/apps/server/src/modules/tool/common/domain/error/tool-parameter-mandatory-value-missing.loggable-exception.spec.ts @@ -1,16 +1,14 @@ import { customParameterFactory } from '@shared/testing'; import { CustomParameter } from '../custom-parameter.do'; -import { ToolParameterValueMissingLoggableException } from './tool-parameter-value-missing.loggable-exception'; +import { ToolParameterMandatoryValueMissingLoggableException } from './tool-parameter-mandatory-value-missing-loggable.exception'; -describe(ToolParameterValueMissingLoggableException.name, () => { +describe(ToolParameterMandatoryValueMissingLoggableException.name, () => { describe('getLogMessage', () => { const setup = () => { const parameter: CustomParameter = customParameterFactory.build(); - const exception: ToolParameterValueMissingLoggableException = new ToolParameterValueMissingLoggableException( - 'toolId', - parameter - ); + const exception: ToolParameterMandatoryValueMissingLoggableException = + new ToolParameterMandatoryValueMissingLoggableException('toolId', parameter); return { parameter, @@ -24,8 +22,8 @@ describe(ToolParameterValueMissingLoggableException.name, () => { const result = exception.getLogMessage(); expect(result).toEqual({ - type: 'TOOL_PARAMETER_VALUE_MISSING', - message: 'The parameter has no value.', + type: 'TOOL_PARAMETER_MANDATORY_VALUE_MISSING', + message: 'The mandatory parameter has no value.', stack: exception.stack, data: { parameterName: parameter.name, diff --git a/apps/server/src/modules/tool/common/domain/error/tool-parameter-optional-value-missing-loggable-exception.spec.ts b/apps/server/src/modules/tool/common/domain/error/tool-parameter-optional-value-missing-loggable-exception.spec.ts new file mode 100644 index 00000000000..2c047219822 --- /dev/null +++ b/apps/server/src/modules/tool/common/domain/error/tool-parameter-optional-value-missing-loggable-exception.spec.ts @@ -0,0 +1,35 @@ +import { customParameterFactory } from '@shared/testing'; +import { CustomParameter } from '../custom-parameter.do'; +import { ToolParameterOptionalValueMissingLoggableException } from './tool-parameter-optional-value-missing-loggable-exception'; + +describe(ToolParameterOptionalValueMissingLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const parameter: CustomParameter = customParameterFactory.build(); + + const exception: ToolParameterOptionalValueMissingLoggableException = + new ToolParameterOptionalValueMissingLoggableException('toolId', parameter); + + return { + parameter, + exception, + }; + }; + + it('should return log message', () => { + const { exception, parameter } = setup(); + + const result = exception.getLogMessage(); + + expect(result).toStrictEqual({ + type: 'VALUE_MISSING_ON_OPTIONAL_TOOL_PARAMETER', + message: 'The optional parameter has no value.', + stack: exception.stack, + data: { + parameterName: parameter.name, + validatableToolId: 'toolId', + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/common/domain/error/tool-parameter-optional-value-missing-loggable-exception.ts b/apps/server/src/modules/tool/common/domain/error/tool-parameter-optional-value-missing-loggable-exception.ts new file mode 100644 index 00000000000..203987dce0b --- /dev/null +++ b/apps/server/src/modules/tool/common/domain/error/tool-parameter-optional-value-missing-loggable-exception.ts @@ -0,0 +1,34 @@ +import { HttpStatus } from '@nestjs/common'; +import { BusinessError } from '@shared/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { EntityId } from '@shared/domain/types'; +import { CustomParameter } from '../custom-parameter.do'; + +export class ToolParameterOptionalValueMissingLoggableException extends BusinessError implements Loggable { + constructor(private readonly validatableToolId: EntityId | undefined, private readonly parameter: CustomParameter) { + super( + { + type: 'VALUE_MISSING_ON_OPTIONAL_TOOL_PARAMETER', + title: 'Missing value on optional tool parameter', + defaultMessage: 'The optional parameter has no value.', + }, + HttpStatus.BAD_REQUEST, + { + parameter, + validatableToolId, + } + ); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: this.type, + message: this.message, + stack: this.stack, + data: { + parameterName: this.parameter.name, + validatableToolId: this.validatableToolId, + }, + }; + } +} diff --git a/apps/server/src/modules/tool/common/domain/index.ts b/apps/server/src/modules/tool/common/domain/index.ts index be02ad54afe..2deae9c5f56 100644 --- a/apps/server/src/modules/tool/common/domain/index.ts +++ b/apps/server/src/modules/tool/common/domain/index.ts @@ -4,7 +4,8 @@ export { ToolParameterUnknownLoggableException, ToolParameterValueRegexLoggableException, ToolParameterTypeMismatchLoggableException, - ToolParameterValueMissingLoggableException, + ToolParameterMandatoryValueMissingLoggableException, + ToolParameterOptionalValueMissingLoggableException, ContextExternalToolNameAlreadyExistsLoggableException, } from './error'; export * from './custom-parameter.do'; diff --git a/apps/server/src/modules/tool/common/mapper/tool-status-response.mapper.ts b/apps/server/src/modules/tool/common/mapper/tool-status-response.mapper.ts index 665da76b84b..8822b2ad1a0 100644 --- a/apps/server/src/modules/tool/common/mapper/tool-status-response.mapper.ts +++ b/apps/server/src/modules/tool/common/mapper/tool-status-response.mapper.ts @@ -8,6 +8,7 @@ export class ToolStatusResponseMapper { isOutdatedOnScopeSchool: status.isOutdatedOnScopeSchool, isOutdatedOnScopeContext: status.isOutdatedOnScopeContext, isIncompleteOnScopeContext: status.isIncompleteOnScopeContext, + isIncompleteOperationalOnScopeContext: status.isIncompleteOperationalOnScopeContext, isDeactivated: status.isDeactivated, }); diff --git a/apps/server/src/modules/tool/common/service/validation/rules/parameter-array-entry-validator.spec.ts b/apps/server/src/modules/tool/common/service/validation/rules/parameter-array-entry-validator.spec.ts index aa9c5ea2114..6704abe4857 100644 --- a/apps/server/src/modules/tool/common/service/validation/rules/parameter-array-entry-validator.spec.ts +++ b/apps/server/src/modules/tool/common/service/validation/rules/parameter-array-entry-validator.spec.ts @@ -4,7 +4,7 @@ import { CustomParameter, CustomParameterEntry, ToolParameterRequiredLoggableException, - ToolParameterValueMissingLoggableException, + ToolParameterMandatoryValueMissingLoggableException, } from '../../../domain'; import { ParameterArrayEntryValidator } from './parameter-array-entry-validator'; @@ -83,7 +83,7 @@ describe(ParameterArrayEntryValidator.name, () => { const result: ValidationError[] = new ParameterArrayEntryValidator().validate(entries, declarations, undefined); - expect(result[0]).toBeInstanceOf(ToolParameterValueMissingLoggableException); + expect(result[0]).toBeInstanceOf(ToolParameterMandatoryValueMissingLoggableException); }); }); }); diff --git a/apps/server/src/modules/tool/common/service/validation/rules/parameter-entry-value-validator.spec.ts b/apps/server/src/modules/tool/common/service/validation/rules/parameter-entry-value-validator.spec.ts index 8acc86f8a7b..4784f3054c8 100644 --- a/apps/server/src/modules/tool/common/service/validation/rules/parameter-entry-value-validator.spec.ts +++ b/apps/server/src/modules/tool/common/service/validation/rules/parameter-entry-value-validator.spec.ts @@ -1,6 +1,11 @@ import { ValidationError } from '@shared/common'; import { customParameterFactory } from '@shared/testing'; -import { CustomParameter, CustomParameterEntry, ToolParameterValueMissingLoggableException } from '../../../domain'; +import { + CustomParameter, + CustomParameterEntry, + ToolParameterMandatoryValueMissingLoggableException, + ToolParameterOptionalValueMissingLoggableException, +} from '../../../domain'; import { ParameterEntryValueValidator } from './parameter-entry-value-validator'; describe(ParameterEntryValueValidator.name, () => { @@ -30,52 +35,107 @@ describe(ParameterEntryValueValidator.name, () => { }); }); - describe('when the parameter value is an empty string', () => { - const setup = () => { - const declaration: CustomParameter = customParameterFactory.build({ - name: 'param1', - }); - const entry: CustomParameterEntry = new CustomParameterEntry({ - name: 'param1', - value: '', + describe('when parameter is mandatory', () => { + describe('when the parameter value is an empty string', () => { + const setup = () => { + const declaration: CustomParameter = customParameterFactory.build({ + name: 'param1', + }); + const entry: CustomParameterEntry = new CustomParameterEntry({ + name: 'param1', + value: '', + }); + + return { + entry, + declaration, + }; + }; + + it('should return a validation error', () => { + const { entry, declaration } = setup(); + + const result: ValidationError[] = new ParameterEntryValueValidator().validate(entry, declaration, undefined); + + expect(result[0]).toBeInstanceOf(ToolParameterMandatoryValueMissingLoggableException); }); + }); - return { - entry, - declaration, + describe('when the parameter value is undefined', () => { + const setup = () => { + const declaration: CustomParameter = customParameterFactory.build({ + name: 'param1', + }); + const entry: CustomParameterEntry = new CustomParameterEntry({ + name: 'param1', + }); + + return { + entry, + declaration, + }; }; - }; - it('should return a validation error', () => { - const { entry, declaration } = setup(); + it('should return a validation error', () => { + const { entry, declaration } = setup(); - const result: ValidationError[] = new ParameterEntryValueValidator().validate(entry, declaration, undefined); + const result: ValidationError[] = new ParameterEntryValueValidator().validate(entry, declaration, undefined); - expect(result[0]).toBeInstanceOf(ToolParameterValueMissingLoggableException); + expect(result[0]).toBeInstanceOf(ToolParameterMandatoryValueMissingLoggableException); + }); }); }); - describe('when the parameter value is undefined', () => { - const setup = () => { - const declaration: CustomParameter = customParameterFactory.build({ - name: 'param1', - }); - const entry: CustomParameterEntry = new CustomParameterEntry({ - name: 'param1', + describe('when parameter is optional', () => { + describe('when the parameter value is an empty string', () => { + const setup = () => { + const declaration: CustomParameter = customParameterFactory.build({ + name: 'param1', + isOptional: true, + }); + const entry: CustomParameterEntry = new CustomParameterEntry({ + name: 'param1', + value: '', + }); + + return { + entry, + declaration, + }; + }; + + it('should return a validation error', () => { + const { entry, declaration } = setup(); + + const result: ValidationError[] = new ParameterEntryValueValidator().validate(entry, declaration, undefined); + + expect(result[0]).toBeInstanceOf(ToolParameterOptionalValueMissingLoggableException); }); + }); - return { - entry, - declaration, + describe('when the parameter value is undefined', () => { + const setup = () => { + const declaration: CustomParameter = customParameterFactory.build({ + name: 'param1', + isOptional: true, + }); + const entry: CustomParameterEntry = new CustomParameterEntry({ + name: 'param1', + }); + + return { + entry, + declaration, + }; }; - }; - it('should return a validation error', () => { - const { entry, declaration } = setup(); + it('should return a validation error', () => { + const { entry, declaration } = setup(); - const result: ValidationError[] = new ParameterEntryValueValidator().validate(entry, declaration, undefined); + const result: ValidationError[] = new ParameterEntryValueValidator().validate(entry, declaration, undefined); - expect(result[0]).toBeInstanceOf(ToolParameterValueMissingLoggableException); + expect(result[0]).toBeInstanceOf(ToolParameterOptionalValueMissingLoggableException); + }); }); }); }); diff --git a/apps/server/src/modules/tool/common/service/validation/rules/parameter-entry-value-validator.ts b/apps/server/src/modules/tool/common/service/validation/rules/parameter-entry-value-validator.ts index ba88e74725b..191367d7c8f 100644 --- a/apps/server/src/modules/tool/common/service/validation/rules/parameter-entry-value-validator.ts +++ b/apps/server/src/modules/tool/common/service/validation/rules/parameter-entry-value-validator.ts @@ -1,6 +1,11 @@ import { ValidationError } from '@shared/common'; import { EntityId } from '@shared/domain/types'; -import { CustomParameter, CustomParameterEntry, ToolParameterValueMissingLoggableException } from '../../../domain'; +import { + CustomParameter, + CustomParameterEntry, + ToolParameterMandatoryValueMissingLoggableException, + ToolParameterOptionalValueMissingLoggableException, +} from '../../../domain'; import { ParameterEntryValidator } from './parameter-entry-validator'; export class ParameterEntryValueValidator implements ParameterEntryValidator { @@ -10,7 +15,10 @@ export class ParameterEntryValueValidator implements ParameterEntryValidator { toolId: EntityId | undefined ): ValidationError[] { if (entry.value === undefined || entry.value === '') { - return [new ToolParameterValueMissingLoggableException(toolId, declaration)]; + if (declaration.isOptional) { + return [new ToolParameterOptionalValueMissingLoggableException(toolId, declaration)]; + } + return [new ToolParameterMandatoryValueMissingLoggableException(toolId, declaration)]; } return []; diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.spec.ts index d4c6d4a55b4..a73384fed12 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.spec.ts @@ -10,7 +10,8 @@ import { import { ContextExternalToolConfigurationStatus, ToolParameterDuplicateLoggableException, - ToolParameterValueMissingLoggableException, + ToolParameterMandatoryValueMissingLoggableException, + ToolParameterOptionalValueMissingLoggableException, } from '../../common/domain'; import { CommonToolValidationService } from '../../common/service'; import { ToolConfigurationStatusService } from './tool-configuration-status.service'; @@ -77,6 +78,7 @@ describe(ToolConfigurationStatusService.name, () => { isOutdatedOnScopeSchool: false, isOutdatedOnScopeContext: false, isIncompleteOnScopeContext: false, + isIncompleteOperationalOnScopeContext: false, isDeactivated: false, }); }); @@ -131,6 +133,7 @@ describe(ToolConfigurationStatusService.name, () => { isOutdatedOnScopeSchool: true, isOutdatedOnScopeContext: false, isIncompleteOnScopeContext: false, + isIncompleteOperationalOnScopeContext: false, isDeactivated: false, }); }); @@ -185,6 +188,7 @@ describe(ToolConfigurationStatusService.name, () => { isOutdatedOnScopeSchool: false, isOutdatedOnScopeContext: true, isIncompleteOnScopeContext: false, + isIncompleteOperationalOnScopeContext: false, isDeactivated: false, }); }); @@ -239,6 +243,7 @@ describe(ToolConfigurationStatusService.name, () => { isOutdatedOnScopeSchool: true, isOutdatedOnScopeContext: true, isIncompleteOnScopeContext: false, + isIncompleteOperationalOnScopeContext: false, isDeactivated: false, }); }); @@ -260,7 +265,7 @@ describe(ToolConfigurationStatusService.name, () => { }); }); - describe('when validation of ContextExternalTool throws at least 1 missing value errors', () => { + describe('when validation of ContextExternalTool throws at least 1 missing value on mandatory parameter errors', () => { const setup = () => { const customParameter = customParameterFactory.build(); const externalTool = externalToolFactory.buildWithId({ parameters: [customParameter] }); @@ -273,8 +278,9 @@ describe(ToolConfigurationStatusService.name, () => { commonToolValidationService.validateParameters.mockReturnValueOnce([]); commonToolValidationService.validateParameters.mockReturnValueOnce([ - new ToolParameterValueMissingLoggableException(undefined, customParameter), + new ToolParameterMandatoryValueMissingLoggableException(undefined, customParameter), new ToolParameterDuplicateLoggableException(undefined, customParameter.name), + new ToolParameterOptionalValueMissingLoggableException(undefined, customParameter), ]); return { @@ -297,6 +303,94 @@ describe(ToolConfigurationStatusService.name, () => { isOutdatedOnScopeSchool: false, isOutdatedOnScopeContext: true, isIncompleteOnScopeContext: true, + isIncompleteOperationalOnScopeContext: false, + isDeactivated: false, + }); + }); + }); + + describe('when validation of ContextExternalTool throws at least 1 missing value on optional parameter errors', () => { + const setup = () => { + const customParameter = customParameterFactory.build(); + const externalTool = externalToolFactory.buildWithId({ parameters: [customParameter] }); + const schoolExternalTool = schoolExternalToolFactory.buildWithId({ + toolId: externalTool.id as string, + }); + const contextExternalTool = contextExternalToolFactory + .withSchoolExternalToolRef(schoolExternalTool.id as string) + .buildWithId(); + + commonToolValidationService.validateParameters.mockReturnValueOnce([]); + commonToolValidationService.validateParameters.mockReturnValueOnce([ + new ToolParameterOptionalValueMissingLoggableException(undefined, customParameter), + new ToolParameterDuplicateLoggableException(undefined, customParameter.name), + ]); + + return { + externalTool, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return incomplete operational as tool status', () => { + const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + + const status: ContextExternalToolConfigurationStatus = service.determineToolConfigurationStatus( + externalTool, + schoolExternalTool, + contextExternalTool + ); + + expect(status).toEqual({ + isOutdatedOnScopeSchool: false, + isOutdatedOnScopeContext: true, + isIncompleteOnScopeContext: false, + isIncompleteOperationalOnScopeContext: true, + isDeactivated: false, + }); + }); + }); + + describe('when validation of ContextExternalTool throws only missing value on optional parameter errors', () => { + const setup = () => { + const customParameter = customParameterFactory.build(); + const externalTool = externalToolFactory.buildWithId({ parameters: [customParameter] }); + const schoolExternalTool = schoolExternalToolFactory.buildWithId({ + toolId: externalTool.id as string, + }); + const contextExternalTool = contextExternalToolFactory + .withSchoolExternalToolRef(schoolExternalTool.id as string) + .buildWithId(); + + commonToolValidationService.validateParameters.mockReturnValueOnce([]); + commonToolValidationService.validateParameters.mockReturnValueOnce([ + new ToolParameterOptionalValueMissingLoggableException(undefined, customParameter), + new ToolParameterOptionalValueMissingLoggableException(undefined, customParameter), + new ToolParameterOptionalValueMissingLoggableException(undefined, customParameter), + ]); + + return { + externalTool, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return incomplete operational as tool status', () => { + const { externalTool, schoolExternalTool, contextExternalTool } = setup(); + + const status: ContextExternalToolConfigurationStatus = service.determineToolConfigurationStatus( + externalTool, + schoolExternalTool, + contextExternalTool + ); + + expect(status).toEqual({ + isOutdatedOnScopeSchool: false, + isOutdatedOnScopeContext: false, + isIncompleteOnScopeContext: false, + isIncompleteOperationalOnScopeContext: true, isDeactivated: false, }); }); @@ -336,6 +430,7 @@ describe(ToolConfigurationStatusService.name, () => { isOutdatedOnScopeSchool: true, isOutdatedOnScopeContext: true, isIncompleteOnScopeContext: false, + isIncompleteOperationalOnScopeContext: false, isDeactivated: true, }); }); @@ -374,6 +469,7 @@ describe(ToolConfigurationStatusService.name, () => { isOutdatedOnScopeSchool: true, isOutdatedOnScopeContext: true, isIncompleteOnScopeContext: false, + isIncompleteOperationalOnScopeContext: false, isDeactivated: true, }); }); @@ -412,6 +508,7 @@ describe(ToolConfigurationStatusService.name, () => { isOutdatedOnScopeSchool: true, isOutdatedOnScopeContext: true, isIncompleteOnScopeContext: false, + isIncompleteOperationalOnScopeContext: false, isDeactivated: false, }); }); diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.ts index a5c4868ba25..56c02af20af 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-configuration-status.service.ts @@ -2,7 +2,8 @@ import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator' import { ValidationError } from '@shared/common'; import { ContextExternalToolConfigurationStatus, - ToolParameterValueMissingLoggableException, + ToolParameterMandatoryValueMissingLoggableException, + ToolParameterOptionalValueMissingLoggableException, } from '../../common/domain'; import { CommonToolValidationService } from '../../common/service'; import { ExternalTool } from '../../external-tool/domain'; @@ -21,6 +22,7 @@ export class ToolConfigurationStatusService { const configurationStatus: ContextExternalToolConfigurationStatus = new ContextExternalToolConfigurationStatus({ isOutdatedOnScopeContext: false, isIncompleteOnScopeContext: false, + isIncompleteOperationalOnScopeContext: false, isOutdatedOnScopeSchool: false, isDeactivated: this.isToolDeactivated(externalTool, schoolExternalTool), }); @@ -44,10 +46,15 @@ export class ToolConfigurationStatusService { if ( contextParameterErrors.some( - (error: ValidationError) => error instanceof ToolParameterValueMissingLoggableException + (error: ValidationError) => error instanceof ToolParameterMandatoryValueMissingLoggableException ) ) { configurationStatus.isIncompleteOnScopeContext = true; + } else if (this.isIncompleteOperational(contextParameterErrors) && !this.isOutdated(contextParameterErrors)) { + configurationStatus.isIncompleteOperationalOnScopeContext = true; + configurationStatus.isOutdatedOnScopeContext = false; + } else if (this.isIncompleteOperational(contextParameterErrors) && this.isOutdated(contextParameterErrors)) { + configurationStatus.isIncompleteOperationalOnScopeContext = true; } } @@ -55,10 +62,22 @@ export class ToolConfigurationStatusService { } private isToolDeactivated(externalTool: ExternalTool, schoolExternalTool: SchoolExternalTool) { - if (externalTool.isDeactivated || (schoolExternalTool.status && schoolExternalTool.status.isDeactivated)) { - return true; - } + return !!(externalTool.isDeactivated || (schoolExternalTool.status && schoolExternalTool.status.isDeactivated)); + } + + private isIncompleteOperational(errors: ValidationError[]) { + return errors.some((error: ValidationError) => error instanceof ToolParameterOptionalValueMissingLoggableException); + } + + private isOutdated(contextParameterErrors: ValidationError[]): boolean { + const parameterWithoutOptional: ValidationError[] = contextParameterErrors.filter( + (error: ValidationError) => !this.isOptional(error) + ); + + return parameterWithoutOptional.length > 0; + } - return false; + isOptional(error: ValidationError): boolean { + return error instanceof ToolParameterOptionalValueMissingLoggableException; } } diff --git a/apps/server/src/modules/tool/tool-launch/error/index.ts b/apps/server/src/modules/tool/tool-launch/error/index.ts index 662cc86f44f..f657499dd46 100644 --- a/apps/server/src/modules/tool/tool-launch/error/index.ts +++ b/apps/server/src/modules/tool/tool-launch/error/index.ts @@ -1,4 +1,4 @@ -export * from './tool-status-outdated.loggable-exception'; +export * from './tool-status-not-launchable.loggable-exception'; export * from './missing-tool-parameter-value.loggable-exception'; export * from './parameter-type-not-implemented.loggable-exception'; export { MissingMediaLicenseLoggableException } from './missing-licence.loggable-exception'; diff --git a/apps/server/src/modules/tool/tool-launch/error/tool-status-outdated.loggable-exception.spec.ts b/apps/server/src/modules/tool/tool-launch/error/tool-status-not-launchable.loggable-exception.spec.ts similarity index 64% rename from apps/server/src/modules/tool/tool-launch/error/tool-status-outdated.loggable-exception.spec.ts rename to apps/server/src/modules/tool/tool-launch/error/tool-status-not-launchable.loggable-exception.spec.ts index 814dfad394c..3c081301fdd 100644 --- a/apps/server/src/modules/tool/tool-launch/error/tool-status-outdated.loggable-exception.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/error/tool-status-not-launchable.loggable-exception.spec.ts @@ -1,18 +1,20 @@ import { toolConfigurationStatusFactory } from '@shared/testing'; -import { ToolStatusOutdatedLoggableException } from './tool-status-outdated.loggable-exception'; +import { ToolStatusNotLaunchableLoggableException } from './tool-status-not-launchable.loggable-exception'; -describe('ToolStatusOutdatedLoggableException', () => { +describe('ToolStatusNotLaunchableLoggableException', () => { describe('getLogMessage', () => { const setup = () => { const toolId = 'toolId'; const userId = 'userId'; const toolConfigStatus = toolConfigurationStatusFactory.build(); - const exception = new ToolStatusOutdatedLoggableException( + const exception = new ToolStatusNotLaunchableLoggableException( userId, toolId, toolConfigStatus.isOutdatedOnScopeSchool, toolConfigStatus.isOutdatedOnScopeContext, + toolConfigStatus.isIncompleteOnScopeContext, + toolConfigStatus.isIncompleteOperationalOnScopeContext, toolConfigStatus.isDeactivated ); @@ -27,7 +29,7 @@ describe('ToolStatusOutdatedLoggableException', () => { const result = exception.getLogMessage(); expect(result).toEqual({ - type: 'TOOL_STATUS_OUTDATED', + type: 'TOOL_STATUS_NOT_LAUNCHABLE', message: expect.any(String), stack: expect.any(String), data: { @@ -35,6 +37,8 @@ describe('ToolStatusOutdatedLoggableException', () => { toolId: 'toolId', isOutdatedOnScopeSchool: false, isOutdatedOnScopeContext: false, + isIncompleteOnScopeContext: false, + isIncompleteOperationalOnScopeContext: false, isDeactivated: false, }, }); diff --git a/apps/server/src/modules/tool/tool-launch/error/tool-status-outdated.loggable-exception.ts b/apps/server/src/modules/tool/tool-launch/error/tool-status-not-launchable.loggable-exception.ts similarity index 62% rename from apps/server/src/modules/tool/tool-launch/error/tool-status-outdated.loggable-exception.ts rename to apps/server/src/modules/tool/tool-launch/error/tool-status-not-launchable.loggable-exception.ts index 84b358e2ec6..c78ff80e764 100644 --- a/apps/server/src/modules/tool/tool-launch/error/tool-status-outdated.loggable-exception.ts +++ b/apps/server/src/modules/tool/tool-launch/error/tool-status-not-launchable.loggable-exception.ts @@ -2,12 +2,14 @@ import { BadRequestException } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; -export class ToolStatusOutdatedLoggableException extends BadRequestException implements Loggable { +export class ToolStatusNotLaunchableLoggableException extends BadRequestException implements Loggable { constructor( private readonly userId: EntityId, private readonly toolId: EntityId, private readonly isOutdatedOnScopeSchool: boolean, private readonly isOutdatedOnScopeContext: boolean, + private readonly isIncompleteOnScopeContext: boolean, + private readonly isIncompleteOperationalOnScopeContext: boolean, private readonly isDeactivated: boolean ) { super(); @@ -15,14 +17,16 @@ export class ToolStatusOutdatedLoggableException extends BadRequestException imp getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { return { - type: 'TOOL_STATUS_OUTDATED', - message: 'The status of the tool is outdated and cannot be launched by the user.', + type: 'TOOL_STATUS_NOT_LAUNCHABLE', + message: 'The status of the tool cannot be launched by the user.', stack: this.stack, data: { userId: this.userId, toolId: this.toolId, isOutdatedOnScopeSchool: this.isOutdatedOnScopeSchool, isOutdatedOnScopeContext: this.isOutdatedOnScopeContext, + isIncompleteOnScopeContext: this.isIncompleteOnScopeContext, + isIncompleteOperationalOnScopeContext: this.isIncompleteOperationalOnScopeContext, isDeactivated: this.isDeactivated, }, }; diff --git a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts index 8d20046076f..3bf4b57df06 100644 --- a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts @@ -14,7 +14,7 @@ import { BasicToolConfig, ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool'; import { SchoolExternalToolWithId } from '../../school-external-tool/domain'; import { SchoolExternalToolService } from '../../school-external-tool'; -import { ToolStatusOutdatedLoggableException } from '../error'; +import { ToolStatusNotLaunchableLoggableException } from '../error'; import { LaunchRequestMethod, ToolLaunchData, ToolLaunchDataType, ToolLaunchRequest } from '../types'; import { BasicToolLaunchStrategy, @@ -23,7 +23,7 @@ import { ToolLaunchParams, } from './launch-strategy'; import { ToolLaunchService } from './tool-launch.service'; -import { ToolConfigurationStatusService } from '../../context-external-tool/service/tool-configuration-status.service'; +import { ToolConfigurationStatusService } from '../../context-external-tool/service'; describe('ToolLaunchService', () => { let module: TestingModule; @@ -236,6 +236,7 @@ describe('ToolLaunchService', () => { isOutdatedOnScopeContext: true, isOutdatedOnScopeSchool: true, isIncompleteOnScopeContext: false, + isIncompleteOperationalOnScopeContext: false, isDeactivated: true, }) ); @@ -247,13 +248,13 @@ describe('ToolLaunchService', () => { }; }; - it('should throw ToolStatusOutdatedLoggableException', async () => { + it('should throw ToolStatusNotLaunchableLoggableException', async () => { const { launchParams, userId, contextExternalToolId } = setup(); const func = () => service.getLaunchData(userId, launchParams.contextExternalTool); await expect(func).rejects.toThrow( - new ToolStatusOutdatedLoggableException(userId, contextExternalToolId, true, true, true) + new ToolStatusNotLaunchableLoggableException(userId, contextExternalToolId, true, true, false, false, true) ); }); }); diff --git a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts index 27ab8f6cf90..b78e7a0eb2e 100644 --- a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts +++ b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts @@ -3,12 +3,12 @@ import { EntityId } from '@shared/domain/types'; import { ContextExternalToolConfigurationStatus } from '../../common/domain'; import { ToolConfigType } from '../../common/enum'; import { ContextExternalTool } from '../../context-external-tool/domain'; -import { ToolConfigurationStatusService } from '../../context-external-tool/service/tool-configuration-status.service'; +import { ToolConfigurationStatusService } from '../../context-external-tool/service'; import { ExternalTool } from '../../external-tool/domain'; -import { ExternalToolService } from '../../external-tool/service'; +import { ExternalToolService } from '../../external-tool'; import { SchoolExternalTool } from '../../school-external-tool/domain'; -import { SchoolExternalToolService } from '../../school-external-tool/service'; -import { ToolStatusOutdatedLoggableException } from '../error'; +import { SchoolExternalToolService } from '../../school-external-tool'; +import { ToolStatusNotLaunchableLoggableException } from '../error'; import { ToolLaunchMapper } from '../mapper'; import { ToolLaunchData, ToolLaunchRequest } from '../types'; import { @@ -96,12 +96,19 @@ export class ToolLaunchService { contextExternalTool ); - if (status.isOutdatedOnScopeSchool || status.isOutdatedOnScopeContext || status.isDeactivated) { - throw new ToolStatusOutdatedLoggableException( + if ( + status.isOutdatedOnScopeSchool || + status.isOutdatedOnScopeContext || + status.isDeactivated || + status.isIncompleteOnScopeContext + ) { + throw new ToolStatusNotLaunchableLoggableException( userId, contextExternalTool.id ?? '', status.isOutdatedOnScopeSchool, status.isOutdatedOnScopeContext, + status.isIncompleteOnScopeContext, + status.isIncompleteOperationalOnScopeContext, status.isDeactivated ); } diff --git a/apps/server/src/shared/testing/factory/context-external-tool-configuration-status-response.factory.ts b/apps/server/src/shared/testing/factory/context-external-tool-configuration-status-response.factory.ts index 9954fd3797f..2075119bf72 100644 --- a/apps/server/src/shared/testing/factory/context-external-tool-configuration-status-response.factory.ts +++ b/apps/server/src/shared/testing/factory/context-external-tool-configuration-status-response.factory.ts @@ -7,6 +7,7 @@ export const contextExternalToolConfigurationStatusResponseFactory = isOutdatedOnScopeContext: false, isOutdatedOnScopeSchool: false, isIncompleteOnScopeContext: false, + isIncompleteOperationalOnScopeContext: false, isDeactivated: false, }; }); diff --git a/apps/server/src/shared/testing/factory/domainobject/tool/tool-configuration-status.factory.ts b/apps/server/src/shared/testing/factory/domainobject/tool/tool-configuration-status.factory.ts index 838c57978c6..64fed88854c 100644 --- a/apps/server/src/shared/testing/factory/domainobject/tool/tool-configuration-status.factory.ts +++ b/apps/server/src/shared/testing/factory/domainobject/tool/tool-configuration-status.factory.ts @@ -6,6 +6,7 @@ export const toolConfigurationStatusFactory = Factory.define