diff --git a/apps/server/src/infra/vidis-client/generated/.openapi-generator/FILES b/apps/server/src/infra/vidis-client/generated/.openapi-generator/FILES index d1183e6e2b4..3d1040f30c3 100644 --- a/apps/server/src/infra/vidis-client/generated/.openapi-generator/FILES +++ b/apps/server/src/infra/vidis-client/generated/.openapi-generator/FILES @@ -1,6 +1,8 @@ .gitignore .npmignore api.ts +api/default-api.ts +api/education-provider-api.ts api/idmbetreiber-api.ts base.ts common.ts diff --git a/apps/server/src/infra/vidis-client/generated/api.ts b/apps/server/src/infra/vidis-client/generated/api.ts index 72a37fe6fec..cb84e884965 100644 --- a/apps/server/src/infra/vidis-client/generated/api.ts +++ b/apps/server/src/infra/vidis-client/generated/api.ts @@ -14,5 +14,7 @@ +export * from './api/default-api'; +export * from './api/education-provider-api'; export * from './api/idmbetreiber-api'; diff --git a/apps/server/src/infra/vidis-client/generated/api/default-api.ts b/apps/server/src/infra/vidis-client/generated/api/default-api.ts new file mode 100644 index 00000000000..ad8d41cb9b2 --- /dev/null +++ b/apps/server/src/infra/vidis-client/generated/api/default-api.ts @@ -0,0 +1,142 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Vidis REST + * Vidis REST API + * + * The version of the OpenAPI document: v1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from '../configuration'; +import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; +/** + * DefaultApi - axios parameter creator + * @export + */ +export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {string} type + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOpenAPI: async (type: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'type' is not null or undefined + assertParamExists('getOpenAPI', 'type', type) + const localVarPath = `/v1.0/openapi.{type}` + .replace(`{${"type"}}`, encodeURIComponent(String(type))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * DefaultApi - functional programming interface + * @export + */ +export const DefaultApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration) + return { + /** + * + * @param {string} type + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getOpenAPI(type: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getOpenAPI(type, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['DefaultApi.getOpenAPI']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * DefaultApi - factory interface + * @export + */ +export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = DefaultApiFp(configuration) + return { + /** + * + * @param {string} type + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOpenAPI(type: string, options?: any): AxiosPromise { + return localVarFp.getOpenAPI(type, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * DefaultApi - interface + * @export + * @interface DefaultApi + */ +export interface DefaultApiInterface { + /** + * + * @param {string} type + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApiInterface + */ + getOpenAPI(type: string, options?: RawAxiosRequestConfig): AxiosPromise; + +} + +/** + * DefaultApi - object-oriented interface + * @export + * @class DefaultApi + * @extends {BaseAPI} + */ +export class DefaultApi extends BaseAPI implements DefaultApiInterface { + /** + * + * @param {string} type + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public getOpenAPI(type: string, options?: RawAxiosRequestConfig) { + return DefaultApiFp(this.configuration).getOpenAPI(type, options).then((request) => request(this.axios, this.basePath)); + } +} + diff --git a/apps/server/src/infra/vidis-client/generated/api/education-provider-api.ts b/apps/server/src/infra/vidis-client/generated/api/education-provider-api.ts new file mode 100644 index 00000000000..e1d5c94cd49 --- /dev/null +++ b/apps/server/src/infra/vidis-client/generated/api/education-provider-api.ts @@ -0,0 +1,676 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Vidis REST + * Vidis REST API + * + * The version of the OpenAPI document: v1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from '../configuration'; +import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; +// @ts-ignore +import type { ActivationDTO } from '../models'; +// @ts-ignore +import type { PageActivationDTO } from '../models'; +// @ts-ignore +import type { PageOfferDTO } from '../models'; +// @ts-ignore +import type { PageSchoolDTO } from '../models'; +/** + * EducationProviderApi - axios parameter creator + * @export + */ +export const EducationProviderApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * List the activation details for a selected owned offer, that has activated by the selected school. + * @param {string} offerId + * @param {string} organizationId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getActivationByOfferAndSchool: async (offerId: string, organizationId: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'offerId' is not null or undefined + assertParamExists('getActivationByOfferAndSchool', 'offerId', offerId) + // verify required parameter 'organizationId' is not null or undefined + assertParamExists('getActivationByOfferAndSchool', 'organizationId', organizationId) + const localVarPath = `/v1.0/activation/offers/{offerId}/schools/{organizationId}/details` + .replace(`{${"offerId"}}`, encodeURIComponent(String(offerId))) + .replace(`{${"organizationId"}}`, encodeURIComponent(String(organizationId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * List the activation details for a selected school, that has activated the selected owned offer. + * @param {string} organizationId + * @param {string} offerId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getActivationBySchoolAndOffer: async (organizationId: string, offerId: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'organizationId' is not null or undefined + assertParamExists('getActivationBySchoolAndOffer', 'organizationId', organizationId) + // verify required parameter 'offerId' is not null or undefined + assertParamExists('getActivationBySchoolAndOffer', 'offerId', offerId) + const localVarPath = `/v1.0/activation/schools/{organizationId}/offers/{offerId}/details` + .replace(`{${"organizationId"}}`, encodeURIComponent(String(organizationId))) + .replace(`{${"offerId"}}`, encodeURIComponent(String(offerId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * List all the activation details for all owned offer. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getActivations: async (page?: string, pageSize?: string, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/v1.0/activation/details`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (pageSize !== undefined) { + localVarQueryParameter['pageSize'] = pageSize; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * List all owned offers, that has activated by any schools. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOffers: async (page?: string, pageSize?: string, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/v1.0/activation/offers`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (pageSize !== undefined) { + localVarQueryParameter['pageSize'] = pageSize; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * List all owned offers, that has activated by the selected school. + * @param {string} organizationId + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOffersBySchool: async (organizationId: string, page?: string, pageSize?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'organizationId' is not null or undefined + assertParamExists('getOffersBySchool', 'organizationId', organizationId) + const localVarPath = `/v1.0/activation/schools/{organizationId}/offers` + .replace(`{${"organizationId"}}`, encodeURIComponent(String(organizationId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (pageSize !== undefined) { + localVarQueryParameter['pageSize'] = pageSize; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * List all schools, that has activated any of the owned offers. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getSchools: async (page?: string, pageSize?: string, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/v1.0/activation/schools`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (pageSize !== undefined) { + localVarQueryParameter['pageSize'] = pageSize; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * List all schools, that has activated the selected owned offer. + * @param {string} offerId + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getSchoolsByOffer: async (offerId: string, page?: string, pageSize?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'offerId' is not null or undefined + assertParamExists('getSchoolsByOffer', 'offerId', offerId) + const localVarPath = `/v1.0/activation/offers/{offerId}/schools` + .replace(`{${"offerId"}}`, encodeURIComponent(String(offerId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (pageSize !== undefined) { + localVarQueryParameter['pageSize'] = pageSize; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * EducationProviderApi - functional programming interface + * @export + */ +export const EducationProviderApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = EducationProviderApiAxiosParamCreator(configuration) + return { + /** + * List the activation details for a selected owned offer, that has activated by the selected school. + * @param {string} offerId + * @param {string} organizationId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getActivationByOfferAndSchool(offerId: string, organizationId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getActivationByOfferAndSchool(offerId, organizationId, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['EducationProviderApi.getActivationByOfferAndSchool']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * List the activation details for a selected school, that has activated the selected owned offer. + * @param {string} organizationId + * @param {string} offerId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getActivationBySchoolAndOffer(organizationId: string, offerId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getActivationBySchoolAndOffer(organizationId, offerId, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['EducationProviderApi.getActivationBySchoolAndOffer']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * List all the activation details for all owned offer. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getActivations(page?: string, pageSize?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getActivations(page, pageSize, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['EducationProviderApi.getActivations']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * List all owned offers, that has activated by any schools. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getOffers(page?: string, pageSize?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getOffers(page, pageSize, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['EducationProviderApi.getOffers']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * List all owned offers, that has activated by the selected school. + * @param {string} organizationId + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getOffersBySchool(organizationId: string, page?: string, pageSize?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getOffersBySchool(organizationId, page, pageSize, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['EducationProviderApi.getOffersBySchool']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * List all schools, that has activated any of the owned offers. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getSchools(page?: string, pageSize?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getSchools(page, pageSize, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['EducationProviderApi.getSchools']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * List all schools, that has activated the selected owned offer. + * @param {string} offerId + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getSchoolsByOffer(offerId: string, page?: string, pageSize?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getSchoolsByOffer(offerId, page, pageSize, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['EducationProviderApi.getSchoolsByOffer']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * EducationProviderApi - factory interface + * @export + */ +export const EducationProviderApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = EducationProviderApiFp(configuration) + return { + /** + * List the activation details for a selected owned offer, that has activated by the selected school. + * @param {string} offerId + * @param {string} organizationId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getActivationByOfferAndSchool(offerId: string, organizationId: string, options?: any): AxiosPromise { + return localVarFp.getActivationByOfferAndSchool(offerId, organizationId, options).then((request) => request(axios, basePath)); + }, + /** + * List the activation details for a selected school, that has activated the selected owned offer. + * @param {string} organizationId + * @param {string} offerId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getActivationBySchoolAndOffer(organizationId: string, offerId: string, options?: any): AxiosPromise { + return localVarFp.getActivationBySchoolAndOffer(organizationId, offerId, options).then((request) => request(axios, basePath)); + }, + /** + * List all the activation details for all owned offer. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getActivations(page?: string, pageSize?: string, options?: any): AxiosPromise { + return localVarFp.getActivations(page, pageSize, options).then((request) => request(axios, basePath)); + }, + /** + * List all owned offers, that has activated by any schools. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOffers(page?: string, pageSize?: string, options?: any): AxiosPromise { + return localVarFp.getOffers(page, pageSize, options).then((request) => request(axios, basePath)); + }, + /** + * List all owned offers, that has activated by the selected school. + * @param {string} organizationId + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOffersBySchool(organizationId: string, page?: string, pageSize?: string, options?: any): AxiosPromise { + return localVarFp.getOffersBySchool(organizationId, page, pageSize, options).then((request) => request(axios, basePath)); + }, + /** + * List all schools, that has activated any of the owned offers. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getSchools(page?: string, pageSize?: string, options?: any): AxiosPromise { + return localVarFp.getSchools(page, pageSize, options).then((request) => request(axios, basePath)); + }, + /** + * List all schools, that has activated the selected owned offer. + * @param {string} offerId + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getSchoolsByOffer(offerId: string, page?: string, pageSize?: string, options?: any): AxiosPromise { + return localVarFp.getSchoolsByOffer(offerId, page, pageSize, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * EducationProviderApi - interface + * @export + * @interface EducationProviderApi + */ +export interface EducationProviderApiInterface { + /** + * List the activation details for a selected owned offer, that has activated by the selected school. + * @param {string} offerId + * @param {string} organizationId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EducationProviderApiInterface + */ + getActivationByOfferAndSchool(offerId: string, organizationId: string, options?: RawAxiosRequestConfig): AxiosPromise; + + /** + * List the activation details for a selected school, that has activated the selected owned offer. + * @param {string} organizationId + * @param {string} offerId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EducationProviderApiInterface + */ + getActivationBySchoolAndOffer(organizationId: string, offerId: string, options?: RawAxiosRequestConfig): AxiosPromise; + + /** + * List all the activation details for all owned offer. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EducationProviderApiInterface + */ + getActivations(page?: string, pageSize?: string, options?: RawAxiosRequestConfig): AxiosPromise; + + /** + * List all owned offers, that has activated by any schools. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EducationProviderApiInterface + */ + getOffers(page?: string, pageSize?: string, options?: RawAxiosRequestConfig): AxiosPromise; + + /** + * List all owned offers, that has activated by the selected school. + * @param {string} organizationId + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EducationProviderApiInterface + */ + getOffersBySchool(organizationId: string, page?: string, pageSize?: string, options?: RawAxiosRequestConfig): AxiosPromise; + + /** + * List all schools, that has activated any of the owned offers. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EducationProviderApiInterface + */ + getSchools(page?: string, pageSize?: string, options?: RawAxiosRequestConfig): AxiosPromise; + + /** + * List all schools, that has activated the selected owned offer. + * @param {string} offerId + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EducationProviderApiInterface + */ + getSchoolsByOffer(offerId: string, page?: string, pageSize?: string, options?: RawAxiosRequestConfig): AxiosPromise; + +} + +/** + * EducationProviderApi - object-oriented interface + * @export + * @class EducationProviderApi + * @extends {BaseAPI} + */ +export class EducationProviderApi extends BaseAPI implements EducationProviderApiInterface { + /** + * List the activation details for a selected owned offer, that has activated by the selected school. + * @param {string} offerId + * @param {string} organizationId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EducationProviderApi + */ + public getActivationByOfferAndSchool(offerId: string, organizationId: string, options?: RawAxiosRequestConfig) { + return EducationProviderApiFp(this.configuration).getActivationByOfferAndSchool(offerId, organizationId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * List the activation details for a selected school, that has activated the selected owned offer. + * @param {string} organizationId + * @param {string} offerId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EducationProviderApi + */ + public getActivationBySchoolAndOffer(organizationId: string, offerId: string, options?: RawAxiosRequestConfig) { + return EducationProviderApiFp(this.configuration).getActivationBySchoolAndOffer(organizationId, offerId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * List all the activation details for all owned offer. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EducationProviderApi + */ + public getActivations(page?: string, pageSize?: string, options?: RawAxiosRequestConfig) { + return EducationProviderApiFp(this.configuration).getActivations(page, pageSize, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * List all owned offers, that has activated by any schools. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EducationProviderApi + */ + public getOffers(page?: string, pageSize?: string, options?: RawAxiosRequestConfig) { + return EducationProviderApiFp(this.configuration).getOffers(page, pageSize, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * List all owned offers, that has activated by the selected school. + * @param {string} organizationId + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EducationProviderApi + */ + public getOffersBySchool(organizationId: string, page?: string, pageSize?: string, options?: RawAxiosRequestConfig) { + return EducationProviderApiFp(this.configuration).getOffersBySchool(organizationId, page, pageSize, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * List all schools, that has activated any of the owned offers. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EducationProviderApi + */ + public getSchools(page?: string, pageSize?: string, options?: RawAxiosRequestConfig) { + return EducationProviderApiFp(this.configuration).getSchools(page, pageSize, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * List all schools, that has activated the selected owned offer. + * @param {string} offerId + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof EducationProviderApi + */ + public getSchoolsByOffer(offerId: string, page?: string, pageSize?: string, options?: RawAxiosRequestConfig) { + return EducationProviderApiFp(this.configuration).getSchoolsByOffer(offerId, page, pageSize, options).then((request) => request(this.axios, this.basePath)); + } +} + diff --git a/apps/server/src/infra/vidis-client/generated/api/idmbetreiber-api.ts b/apps/server/src/infra/vidis-client/generated/api/idmbetreiber-api.ts index 5b95bd1390c..f41affcfc54 100644 --- a/apps/server/src/infra/vidis-client/generated/api/idmbetreiber-api.ts +++ b/apps/server/src/infra/vidis-client/generated/api/idmbetreiber-api.ts @@ -63,6 +63,88 @@ export const IDMBetreiberApiAxiosParamCreator = function (configuration?: Config + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * List all offers, that has activated by the selected school. + * @param {string} schoolName + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getActivatedOffersBySchool: async (schoolName: string, page?: string, pageSize?: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'schoolName' is not null or undefined + assertParamExists('getActivatedOffersBySchool', 'schoolName', schoolName) + const localVarPath = `/v1.0/offers/activated/by-school/{schoolName}` + .replace(`{${"schoolName"}}`, encodeURIComponent(String(schoolName))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (pageSize !== undefined) { + localVarQueryParameter['pageSize'] = pageSize; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * List all offers. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllOffers: async (page?: string, pageSize?: string, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/v1.0/offers/all`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (pageSize !== undefined) { + localVarQueryParameter['pageSize'] = pageSize; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -96,6 +178,33 @@ export const IDMBetreiberApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['IDMBetreiberApi.getActivatedOffersByRegion']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * List all offers, that has activated by the selected school. + * @param {string} schoolName + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getActivatedOffersBySchool(schoolName: string, page?: string, pageSize?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getActivatedOffersBySchool(schoolName, page, pageSize, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['IDMBetreiberApi.getActivatedOffersBySchool']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * List all offers. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAllOffers(page?: string, pageSize?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllOffers(page, pageSize, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['IDMBetreiberApi.getAllOffers']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, } }; @@ -117,6 +226,27 @@ export const IDMBetreiberApiFactory = function (configuration?: Configuration, b getActivatedOffersByRegion(regionName: string, page?: string, pageSize?: string, options?: any): AxiosPromise { return localVarFp.getActivatedOffersByRegion(regionName, page, pageSize, options).then((request) => request(axios, basePath)); }, + /** + * List all offers, that has activated by the selected school. + * @param {string} schoolName + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getActivatedOffersBySchool(schoolName: string, page?: string, pageSize?: string, options?: any): AxiosPromise { + return localVarFp.getActivatedOffersBySchool(schoolName, page, pageSize, options).then((request) => request(axios, basePath)); + }, + /** + * List all offers. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllOffers(page?: string, pageSize?: string, options?: any): AxiosPromise { + return localVarFp.getAllOffers(page, pageSize, options).then((request) => request(axios, basePath)); + }, }; }; @@ -137,6 +267,27 @@ export interface IDMBetreiberApiInterface { */ getActivatedOffersByRegion(regionName: string, page?: string, pageSize?: string, options?: RawAxiosRequestConfig): AxiosPromise; + /** + * List all offers, that has activated by the selected school. + * @param {string} schoolName + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof IDMBetreiberApiInterface + */ + getActivatedOffersBySchool(schoolName: string, page?: string, pageSize?: string, options?: RawAxiosRequestConfig): AxiosPromise; + + /** + * List all offers. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof IDMBetreiberApiInterface + */ + getAllOffers(page?: string, pageSize?: string, options?: RawAxiosRequestConfig): AxiosPromise; + } /** @@ -158,5 +309,30 @@ export class IDMBetreiberApi extends BaseAPI implements IDMBetreiberApiInterface public getActivatedOffersByRegion(regionName: string, page?: string, pageSize?: string, options?: RawAxiosRequestConfig) { return IDMBetreiberApiFp(this.configuration).getActivatedOffersByRegion(regionName, page, pageSize, options).then((request) => request(this.axios, this.basePath)); } + + /** + * List all offers, that has activated by the selected school. + * @param {string} schoolName + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof IDMBetreiberApi + */ + public getActivatedOffersBySchool(schoolName: string, page?: string, pageSize?: string, options?: RawAxiosRequestConfig) { + return IDMBetreiberApiFp(this.configuration).getActivatedOffersBySchool(schoolName, page, pageSize, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * List all offers. + * @param {string} [page] + * @param {string} [pageSize] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof IDMBetreiberApi + */ + public getAllOffers(page?: string, pageSize?: string, options?: RawAxiosRequestConfig) { + return IDMBetreiberApiFp(this.configuration).getAllOffers(page, pageSize, options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/apps/server/src/migrations/mikro-orm/Migration20250121203707.ts b/apps/server/src/migrations/mikro-orm/Migration20250121203707.ts new file mode 100644 index 00000000000..ec65dfa7e23 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20250121203707.ts @@ -0,0 +1,37 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20250121203707 extends Migration { + async up(): Promise { + const adminRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'administrator' }, + { + $addToSet: { + permissions: { + $each: ['MEDIA_SCHOOL_LICENSE_ADMIN'], + }, + }, + } + ); + + if (adminRoleUpdate.modifiedCount > 0) { + console.info('Permission MEDIA_SCHOOL_LICENSE_ADMIN added to role administrator.'); + } + } + + async down(): Promise { + const adminRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'administrator' }, + { + $pull: { + permissions: { + $in: ['MEDIA_SCHOOL_LICENSE_ADMIN'], + }, + }, + } + ); + + if (adminRoleUpdate.modifiedCount > 0) { + console.info('Rollback: Permission MEDIA_SCHOOL_LICENSE_ADMIN added to role administrator.'); + } + } +} diff --git a/apps/server/src/modules/school-license/api/api-test/school-license.api.spec.ts b/apps/server/src/modules/school-license/api/api-test/school-license.api.spec.ts new file mode 100644 index 00000000000..b948936616f --- /dev/null +++ b/apps/server/src/modules/school-license/api/api-test/school-license.api.spec.ts @@ -0,0 +1,182 @@ +import { vidisPageOfferFactory } from '@infra/sync/media-licenses/testing'; +import { PageOfferDTO } from '@infra/vidis-client'; +import { EntityManager } from '@mikro-orm/mongodb'; +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { Permission } from '@shared/domain/interface'; +import { cleanupCollections } from '@testing/cleanup-collections'; +import { schoolEntityFactory } from '@testing/factory/school-entity.factory'; + +import { UserAndAccountTestFactory } from '@testing/factory/user-and-account.test.factory'; +import { TestApiClient } from '@testing/test-api-client'; +import { ServerTestModule } from '@modules/server'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { federalStateFactory } from '@testing/factory/federal-state.factory'; +import { MediaSourceDataFormat } from '@modules/media-source'; +import { mediaSourceEntityFactory } from '@modules/media-source/testing'; + +describe('SchoolLicenseController (API)', () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + let axiosMock: MockAdapter; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + em = app.get(EntityManager); + + axiosMock = new MockAdapter(axios); + + testApiClient = new TestApiClient(app, 'school-licenses'); + }); + + beforeEach(async () => { + await cleanupCollections(em); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('POST /school-licenses', () => { + describe('when the user is not authenticated', () => { + it('should return a 401 error', async () => { + const response = await testApiClient.post(); + + expect(response.status).toBe(HttpStatus.UNAUTHORIZED); + }); + }); + + describe('when update media school licenses was successful', () => { + const setup = async () => { + const federalState = federalStateFactory.build(); + const school = schoolEntityFactory.buildWithId({ + officialSchoolNumber: '00100', + federalState, + }); + const mediaSource = mediaSourceEntityFactory.build({ format: MediaSourceDataFormat.VIDIS }); + + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ + Permission.MEDIA_SCHOOL_LICENSE_ADMIN, + ]); + await em.persistAndFlush([adminUser, adminAccount, federalState, school, mediaSource]); + em.clear(); + + const loggedInClient = await testApiClient.login(adminAccount); + + const pageOfferDTO = vidisPageOfferFactory.build(); + + axiosMock.onGet(/\/offers\/activated\/by-school\/[^/]+$/).replyOnce(200, { + ...pageOfferDTO, + }); + + return { loggedInClient }; + }; + + it('should return status created', async () => { + const { loggedInClient } = await setup(); + + await loggedInClient.post('').send().expect(HttpStatus.CREATED); + }); + }); + + describe('when official school number was not found', () => { + const setup = async () => { + const school = schoolEntityFactory.buildWithId({}); + const mediaSource = mediaSourceEntityFactory.build({ format: MediaSourceDataFormat.VIDIS }); + + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ + Permission.MEDIA_SCHOOL_LICENSE_ADMIN, + ]); + + await em.persistAndFlush([adminUser, adminAccount, school, mediaSource]); + em.clear(); + + const loggedInClient = await testApiClient.login(adminAccount); + + const pageOfferDTO = vidisPageOfferFactory.build(); + + axiosMock.onGet(/\/offers\/activated\/by-school\/[^/]+$/).replyOnce(200, { + ...pageOfferDTO, + }); + + return { loggedInClient }; + }; + + it('should return status not found', async () => { + const { loggedInClient } = await setup(); + + const response = await loggedInClient.post('').send(); + + expect(response.status).toBe(HttpStatus.NOT_FOUND); + }); + }); + + describe('when media source not found', () => { + const setup = async () => { + const school = schoolEntityFactory.buildWithId({ + officialSchoolNumber: '00100', + }); + + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ + Permission.MEDIA_SCHOOL_LICENSE_ADMIN, + ]); + await em.persistAndFlush([adminUser, adminAccount, school]); + em.clear(); + + const loggedInClient = await testApiClient.login(adminAccount); + + const pageOfferDTO = vidisPageOfferFactory.build(); + + axiosMock.onGet(/\/offers\/activated\/by-school\/[^/]+$/).replyOnce(200, { + ...pageOfferDTO, + }); + + return { loggedInClient }; + }; + + it('should return status not found', async () => { + const { loggedInClient } = await setup(); + + const response = await loggedInClient.post('').send(); + + expect(response.status).toBe(HttpStatus.NOT_FOUND); + }); + }); + + describe('when user has no permission', () => { + const setup = async () => { + const school = schoolEntityFactory.buildWithId({ officialSchoolNumber: '00100' }); + const mediaSource = mediaSourceEntityFactory.build({ format: MediaSourceDataFormat.VIDIS }); + + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }); + await em.persistAndFlush([adminUser, adminAccount, school, mediaSource]); + em.clear(); + + const loggedInClient = await testApiClient.login(adminAccount); + + const pageOfferDTO = vidisPageOfferFactory.build(); + + axiosMock.onGet(/\/offers\/activated\/by-school\/[^/]+$/).replyOnce(200, { + ...pageOfferDTO, + }); + + return { loggedInClient }; + }; + + it('should return status unauthorized', async () => { + const { loggedInClient } = await setup(); + + const response = await loggedInClient.post('').send(); + + expect(response.status).toBe(HttpStatus.UNAUTHORIZED); + }); + }); + }); +}); diff --git a/apps/server/src/modules/school-license/api/school-license.controller.ts b/apps/server/src/modules/school-license/api/school-license.controller.ts new file mode 100644 index 00000000000..b8ce885530f --- /dev/null +++ b/apps/server/src/modules/school-license/api/school-license.controller.ts @@ -0,0 +1,19 @@ +import { CurrentUser, ICurrentUser, JwtAuthentication } from '@infra/auth-guard'; +import { Controller, Post } from '@nestjs/common'; +import { ApiCreatedResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { MediaSchoolLicenseUc } from '../uc'; + +@ApiTags('School License') +@JwtAuthentication() +@Controller('school-licenses') +export class SchoolLicenseController { + constructor(private readonly mediaSchoolLicenseUc: MediaSchoolLicenseUc) {} + + @ApiOperation({ summary: 'Update media school licenses' }) + @ApiCreatedResponse() + @ApiUnauthorizedResponse() + @Post() + async updateMediaSchoolLicenses(@CurrentUser() currentUser: ICurrentUser): Promise { + await this.mediaSchoolLicenseUc.updateMediaSchoolLicenses(currentUser.userId, currentUser.schoolId); + } +} diff --git a/apps/server/src/modules/school-license/loggable/build-media-school-license-failed.loggable.spec.ts b/apps/server/src/modules/school-license/loggable/build-media-school-license-failed.loggable.spec.ts new file mode 100644 index 00000000000..1d51ce1c0d3 --- /dev/null +++ b/apps/server/src/modules/school-license/loggable/build-media-school-license-failed.loggable.spec.ts @@ -0,0 +1,23 @@ +import { BuildMediaSchoolLicenseFailedLoggable } from './build-media-school-license-failed.loggable'; + +describe(BuildMediaSchoolLicenseFailedLoggable.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const loggable = new BuildMediaSchoolLicenseFailedLoggable(); + + return { + loggable, + }; + }; + + it('should return the correct log message', () => { + const { loggable } = setup(); + + const logMessage = loggable.getLogMessage(); + + expect(logMessage).toEqual({ + message: 'Unable to build media school license, because mediumId is missing.', + }); + }); + }); +}); diff --git a/apps/server/src/modules/school-license/loggable/build-media-school-license-failed.loggable.ts b/apps/server/src/modules/school-license/loggable/build-media-school-license-failed.loggable.ts new file mode 100644 index 00000000000..2efd391a6cf --- /dev/null +++ b/apps/server/src/modules/school-license/loggable/build-media-school-license-failed.loggable.ts @@ -0,0 +1,9 @@ +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@core/logger'; + +export class BuildMediaSchoolLicenseFailedLoggable implements Loggable { + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + message: 'Unable to build media school license, because mediumId is missing.', + }; + } +} diff --git a/apps/server/src/modules/school-license/loggable/federal-state-abbreviation-of-school-not-found.loggable-exception.spec.ts b/apps/server/src/modules/school-license/loggable/federal-state-abbreviation-of-school-not-found.loggable-exception.spec.ts new file mode 100644 index 00000000000..e28b6cbd71f --- /dev/null +++ b/apps/server/src/modules/school-license/loggable/federal-state-abbreviation-of-school-not-found.loggable-exception.spec.ts @@ -0,0 +1,27 @@ +import { FederalStateAbbreviationOfSchoolNotFoundLoggableException } from './federal-state-abbreviation-of-school-not-found.loggable-exception'; + +describe(FederalStateAbbreviationOfSchoolNotFoundLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const exception = new FederalStateAbbreviationOfSchoolNotFoundLoggableException('testSchoolId'); + + return { + exception, + }; + }; + + it('should return the correct log message', () => { + const { exception } = setup(); + + const logMessage = exception.getLogMessage(); + + expect(logMessage).toEqual({ + message: + 'Unable to fetch media school licenses from media source, because federal state abbreviation of school cannot be found.', + data: { + schoolId: 'testSchoolId', + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/school-license/loggable/federal-state-abbreviation-of-school-not-found.loggable-exception.ts b/apps/server/src/modules/school-license/loggable/federal-state-abbreviation-of-school-not-found.loggable-exception.ts new file mode 100644 index 00000000000..77c3b016659 --- /dev/null +++ b/apps/server/src/modules/school-license/loggable/federal-state-abbreviation-of-school-not-found.loggable-exception.ts @@ -0,0 +1,18 @@ +import { NotFoundException } from '@nestjs/common'; +import { ErrorLogMessage, LogMessage, ValidationErrorLogMessage } from '@core/logger'; + +export class FederalStateAbbreviationOfSchoolNotFoundLoggableException extends NotFoundException { + constructor(private readonly schoolId: string) { + super(); + } + + public getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + message: + 'Unable to fetch media school licenses from media source, because federal state abbreviation of school cannot be found.', + data: { + schoolId: this.schoolId, + }, + }; + } +} diff --git a/apps/server/src/modules/school-license/loggable/index.ts b/apps/server/src/modules/school-license/loggable/index.ts new file mode 100644 index 00000000000..a0fc6918d34 --- /dev/null +++ b/apps/server/src/modules/school-license/loggable/index.ts @@ -0,0 +1,4 @@ +export { BuildMediaSchoolLicenseFailedLoggable } from './build-media-school-license-failed.loggable'; +export { SchoolNumberNotFoundLoggableException } from './school-number-not-found.loggable-exception'; +export { MediaSourceNotFoundLoggableException } from './media-source-not-found.loggable-exception'; +export { FederalStateAbbreviationOfSchoolNotFoundLoggableException } from './federal-state-abbreviation-of-school-not-found.loggable-exception'; diff --git a/apps/server/src/modules/school-license/loggable/media-source-not-found.loggable-exception.spec.ts b/apps/server/src/modules/school-license/loggable/media-source-not-found.loggable-exception.spec.ts new file mode 100644 index 00000000000..8d406a97a1f --- /dev/null +++ b/apps/server/src/modules/school-license/loggable/media-source-not-found.loggable-exception.spec.ts @@ -0,0 +1,26 @@ +import { MediaSourceNotFoundLoggableException } from './media-source-not-found.loggable-exception'; + +describe(MediaSourceNotFoundLoggableException.name, () => { + const setup = () => { + const mediaSourceName = 'TestMediaSource'; + const exception = new MediaSourceNotFoundLoggableException(mediaSourceName); + + return { + exception, + mediaSourceName, + }; + }; + + it('should return the correct log message', () => { + const { exception, mediaSourceName } = setup(); + + const logMessage = exception.getLogMessage(); + + expect(logMessage).toEqual({ + message: 'Unable to fetch media school licenses, because media source cannot be found.', + data: { + mediaSourceName, + }, + }); + }); +}); diff --git a/apps/server/src/modules/school-license/loggable/media-source-not-found.loggable-exception.ts b/apps/server/src/modules/school-license/loggable/media-source-not-found.loggable-exception.ts new file mode 100644 index 00000000000..b6b0e439cf4 --- /dev/null +++ b/apps/server/src/modules/school-license/loggable/media-source-not-found.loggable-exception.ts @@ -0,0 +1,17 @@ +import { NotFoundException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@core/logger'; + +export class MediaSourceNotFoundLoggableException extends NotFoundException implements Loggable { + constructor(private readonly mediaSourceName: string) { + super(); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + message: 'Unable to fetch media school licenses, because media source cannot be found.', + data: { + mediaSourceName: this.mediaSourceName, + }, + }; + } +} diff --git a/apps/server/src/modules/school-license/loggable/school-number-not-found.loggable-exception.spec.ts b/apps/server/src/modules/school-license/loggable/school-number-not-found.loggable-exception.spec.ts new file mode 100644 index 00000000000..7895837b823 --- /dev/null +++ b/apps/server/src/modules/school-license/loggable/school-number-not-found.loggable-exception.spec.ts @@ -0,0 +1,26 @@ +import { SchoolNumberNotFoundLoggableException } from './school-number-not-found.loggable-exception'; + +describe(SchoolNumberNotFoundLoggableException.name, () => { + const setup = () => { + const schoolId = 'testSchoolId'; + const exception = new SchoolNumberNotFoundLoggableException(schoolId); + + return { + exception, + schoolId, + }; + }; + + it('should return the correct log message', () => { + const { exception, schoolId } = setup(); + + const logMessage = exception.getLogMessage(); + + expect(logMessage).toEqual({ + message: 'Required school number for media school licenses request is missing.', + data: { + schoolId, + }, + }); + }); +}); diff --git a/apps/server/src/modules/school-license/loggable/school-number-not-found.loggable-exception.ts b/apps/server/src/modules/school-license/loggable/school-number-not-found.loggable-exception.ts new file mode 100644 index 00000000000..c70b71f6e74 --- /dev/null +++ b/apps/server/src/modules/school-license/loggable/school-number-not-found.loggable-exception.ts @@ -0,0 +1,17 @@ +import { NotFoundException } from '@nestjs/common'; +import { ErrorLogMessage, LogMessage, ValidationErrorLogMessage } from '@core/logger'; + +export class SchoolNumberNotFoundLoggableException extends NotFoundException { + constructor(private readonly schoolId: string) { + super(); + } + + public getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + message: 'Required school number for media school licenses request is missing.', + data: { + schoolId: this.schoolId, + }, + }; + } +} diff --git a/apps/server/src/modules/school-license/repo/media-school-license-repo.interface.ts b/apps/server/src/modules/school-license/repo/media-school-license-repo.interface.ts index 98b43ef0f39..99abd920de9 100644 --- a/apps/server/src/modules/school-license/repo/media-school-license-repo.interface.ts +++ b/apps/server/src/modules/school-license/repo/media-school-license-repo.interface.ts @@ -7,6 +7,8 @@ export interface MediaSchoolLicenseRepo { deleteAllByMediaSource(mediaSourceId: EntityId): Promise; findMediaSchoolLicensesBySchoolId(schoolId: string): Promise; + + deleteAllBySchoolAndMediaSource(schoolId: EntityId, mediaSourceId: EntityId): Promise; } export const MEDIA_SCHOOL_LICENSE_REPO = 'MEDIA_SCHOOL_LICENSE_REPO'; diff --git a/apps/server/src/modules/school-license/repo/mikro-orm/media-school-license.repo.integration.spec.ts b/apps/server/src/modules/school-license/repo/mikro-orm/media-school-license.repo.integration.spec.ts index 87a912ecd44..798288d2f4c 100644 --- a/apps/server/src/modules/school-license/repo/mikro-orm/media-school-license.repo.integration.spec.ts +++ b/apps/server/src/modules/school-license/repo/mikro-orm/media-school-license.repo.integration.spec.ts @@ -141,4 +141,38 @@ describe(MediaSchoolLicenseMikroOrmRepo.name, () => { }); }); }); + + describe('deleteAllBySchoolAndMediaSource', () => { + describe('when a school id and media source id is provided', () => { + const setup = async () => { + const school = schoolEntityFactory.buildWithId(); + const mediaSource = mediaSourceEntityFactory.build(); + const mediaSchoolLicenseEntities: MediaSchoolLicenseEntity[] = mediaSchoolLicenseEntityFactory.buildList(3, { + mediaSource, + school, + }); + + const otherMediaSource = mediaSourceEntityFactory.build(); + const otherEntity = mediaSchoolLicenseEntityFactory.build({ + mediaSource: otherMediaSource, + school, + }); + + await em.persistAndFlush([...mediaSchoolLicenseEntities, otherEntity, school]); + em.clear(); + + return { mediaSource, otherEntity, school }; + }; + + it('should delete the media school license', async () => { + const { mediaSource, otherEntity, school } = await setup(); + + await repo.deleteAllBySchoolAndMediaSource(school.id, mediaSource.id); + + const savedMediaSchoolLicenses: MediaSchoolLicenseEntity[] = await em.find(MediaSchoolLicenseEntity, {}); + expect(savedMediaSchoolLicenses.length).toEqual(1); + expect(savedMediaSchoolLicenses[0]._id.toHexString()).toEqual(otherEntity._id.toHexString()); + }); + }); + }); }); diff --git a/apps/server/src/modules/school-license/repo/mikro-orm/media-school-license.repo.ts b/apps/server/src/modules/school-license/repo/mikro-orm/media-school-license.repo.ts index 81b1be94ef1..3031ed65bca 100644 --- a/apps/server/src/modules/school-license/repo/mikro-orm/media-school-license.repo.ts +++ b/apps/server/src/modules/school-license/repo/mikro-orm/media-school-license.repo.ts @@ -38,6 +38,15 @@ export class MediaSchoolLicenseMikroOrmRepo return deleteCount; } + public async deleteAllBySchoolAndMediaSource(schoolId: EntityId, mediaSourceId: EntityId): Promise { + const deleteCount = await this.em.nativeDelete(this.entityName, { + school: new ObjectId(schoolId), + mediaSource: new ObjectId(mediaSourceId), + }); + + return deleteCount; + } + public async findMediaSchoolLicensesBySchoolId(schoolId: EntityId): Promise { const scope: MediaSchoolLicenseScope = new MediaSchoolLicenseScope(); scope.bySchoolId(schoolId); diff --git a/apps/server/src/modules/school-license/school-license-api.module.ts b/apps/server/src/modules/school-license/school-license-api.module.ts new file mode 100644 index 00000000000..f42cbf51d7a --- /dev/null +++ b/apps/server/src/modules/school-license/school-license-api.module.ts @@ -0,0 +1,12 @@ +import { AuthorizationModule } from '@modules/authorization'; +import { Module } from '@nestjs/common'; +import { SchoolLicenseController } from './api/school-license.controller'; +import { SchoolLicenseModule } from './school-license.module'; +import { MediaSchoolLicenseUc } from './uc'; + +@Module({ + imports: [SchoolLicenseModule, AuthorizationModule], + controllers: [SchoolLicenseController], + providers: [MediaSchoolLicenseUc], +}) +export class SchoolLicenseApiModule {} diff --git a/apps/server/src/modules/school-license/school-license.module.ts b/apps/server/src/modules/school-license/school-license.module.ts index 2461c9a5ea7..cc1dfb53e9d 100644 --- a/apps/server/src/modules/school-license/school-license.module.ts +++ b/apps/server/src/modules/school-license/school-license.module.ts @@ -1,16 +1,18 @@ -import { Module } from '@nestjs/common'; import { LoggerModule } from '@core/logger'; -import { MediaSourceModule } from '../media-source/media-source.module'; -import { SchoolModule } from '../school'; +import { EncryptionModule } from '@infra/encryption'; +import { MediaSourceModule } from '@modules/media-source/media-source.module'; +import { SchoolModule } from '@modules/school'; +import { Module } from '@nestjs/common'; import { MEDIA_SCHOOL_LICENSE_REPO } from './repo'; import { MediaSchoolLicenseMikroOrmRepo } from './repo/mikro-orm/media-school-license.repo'; -import { MediaSchoolLicenseService } from './service/media-school-license.service'; +import { MediaSchoolLicenseFetchService, MediaSchoolLicenseService } from './service'; @Module({ - imports: [MediaSourceModule, SchoolModule, LoggerModule], + imports: [MediaSourceModule, SchoolModule, LoggerModule, EncryptionModule], providers: [ - MediaSchoolLicenseService, { provide: MEDIA_SCHOOL_LICENSE_REPO, useClass: MediaSchoolLicenseMikroOrmRepo }, + MediaSchoolLicenseService, + MediaSchoolLicenseFetchService, ], exports: [MediaSchoolLicenseService], }) diff --git a/apps/server/src/modules/school-license/service/index.ts b/apps/server/src/modules/school-license/service/index.ts new file mode 100644 index 00000000000..67287a15af4 --- /dev/null +++ b/apps/server/src/modules/school-license/service/index.ts @@ -0,0 +1,2 @@ +export { MediaSchoolLicenseService } from './media-school-license.service'; +export { MediaSchoolLicenseFetchService } from './media-school-license-fetch.service'; diff --git a/apps/server/src/modules/school-license/service/media-school-license-fetch.service.spec.ts b/apps/server/src/modules/school-license/service/media-school-license-fetch.service.spec.ts new file mode 100644 index 00000000000..2081040caf0 --- /dev/null +++ b/apps/server/src/modules/school-license/service/media-school-license-fetch.service.spec.ts @@ -0,0 +1,243 @@ +import { AxiosErrorLoggable } from '@core/error/loggable'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { DefaultEncryptionService, EncryptionService, SymmetricKeyEncryptionService } from '@infra/encryption'; +import { vidisPageOfferFactory } from '@infra/sync/media-licenses/testing'; +import { Configuration, IDMBetreiberApiFactory, IDMBetreiberApiInterface, PageOfferDTO } from '@infra/vidis-client'; +import { MediaSourceDataFormat } from '@modules/media-source'; +import { mediaSourceFactory } from '@modules/media-source/testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { axiosErrorFactory } from '@testing/factory/axios-error.factory'; +import { axiosResponseFactory } from '@testing/factory/axios-response.factory'; +import { AxiosResponse, RawAxiosRequestConfig } from 'axios'; +import { MediaSourceVidisConfigNotFoundLoggableException } from '../../media-source/loggable'; +import { MediaSchoolLicenseFetchService } from './media-school-license-fetch.service'; + +jest.mock('@infra/vidis-client/generated/api'); + +describe(MediaSchoolLicenseFetchService.name, () => { + let module: TestingModule; + let service: MediaSchoolLicenseFetchService; + + let encryptionService: DeepMocked; + + let vidisApi: DeepMocked; + let apiFactoryMock: jest.Mocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + MediaSchoolLicenseFetchService, + { + provide: DefaultEncryptionService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(MediaSchoolLicenseFetchService); + encryptionService = module.get(DefaultEncryptionService); + + vidisApi = createMock(); + apiFactoryMock = jest.mocked(IDMBetreiberApiFactory).mockReturnValue(vidisApi); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('fetchOffersForSchoolFromVidis', () => { + describe('when the media source has basic auth config', () => { + describe('when vidis returns the offer items successfully', () => { + const setup = () => { + const mediaSource = mediaSourceFactory.withVidis().build(); + + const axiosResponse = axiosResponseFactory.build({ + data: vidisPageOfferFactory.build(), + }) as AxiosResponse; + + vidisApi.getActivatedOffersBySchool.mockResolvedValueOnce(axiosResponse); + + const decryptedUsername = 'un-decrypted'; + const decryptedPassword = 'pw-decrypted'; + encryptionService.decrypt.mockReturnValueOnce(decryptedUsername); + encryptionService.decrypt.mockReturnValueOnce(decryptedPassword); + + const schoolName = 'NI_12345'; + + return { + mediaSource, + vidisOfferItems: axiosResponse.data.items, + decryptedUsername, + decryptedPassword, + schoolName, + }; + }; + + it('should return the vidis offer items', async () => { + const { mediaSource, vidisOfferItems, schoolName } = setup(); + + const result = await service.fetchOffersForSchoolFromVidis(mediaSource, schoolName); + + expect(result).toEqual(vidisOfferItems); + }); + + it('should decrypt the credentials from basic auth config', async () => { + const { mediaSource, schoolName } = setup(); + + await service.fetchOffersForSchoolFromVidis(mediaSource, schoolName); + + expect(encryptionService.decrypt).toBeCalledTimes(2); + expect(encryptionService.decrypt).toBeCalledWith(mediaSource.vidisConfig?.username); + expect(encryptionService.decrypt).toBeCalledWith(mediaSource.vidisConfig?.password); + }); + + it('should create a vidis api client', async () => { + const { mediaSource, schoolName } = setup(); + + await service.fetchOffersForSchoolFromVidis(mediaSource, schoolName); + + expect(apiFactoryMock).toHaveBeenCalledWith( + new Configuration({ + basePath: mediaSource.vidisConfig?.baseUrl, + }) + ); + }); + + it('should call the vidis endpoint for activated offer items with basic auth', async () => { + const { mediaSource, decryptedUsername, decryptedPassword, schoolName } = setup(); + + await service.fetchOffersForSchoolFromVidis(mediaSource, schoolName); + + const encodedBasicAuth = btoa(`${decryptedUsername}:${decryptedPassword}`); + const expectedAxiosOptions: RawAxiosRequestConfig = { + headers: { Authorization: expect.stringMatching(`Basic ${encodedBasicAuth}`) as string }, + }; + + expect(vidisApi.getActivatedOffersBySchool).toBeCalledWith( + 'NI_12345', + undefined, + undefined, + expectedAxiosOptions + ); + }); + }); + + describe('when vidis returns the no offer items', () => { + const setup = () => { + const mediaSource = mediaSourceFactory.withVidis().build(); + + const axiosResponse = axiosResponseFactory.build({ + data: vidisPageOfferFactory.build({ items: undefined }), + }) as AxiosResponse; + + vidisApi.getActivatedOffersBySchool.mockResolvedValueOnce(axiosResponse); + + encryptionService.decrypt.mockReturnValueOnce('un-decrypted'); + encryptionService.decrypt.mockReturnValueOnce('pw-decrypted'); + + const schoolName = 'NI_12345'; + + return { + mediaSource, + schoolName, + }; + }; + + it('should return an empty array', async () => { + const { mediaSource, schoolName } = setup(); + + const result = await service.fetchOffersForSchoolFromVidis(mediaSource, schoolName); + + expect(result.length).toEqual(0); + }); + }); + + describe('when an axios error is thrown', () => { + const setup = () => { + const mediaSource = mediaSourceFactory.withVidis().build(); + + const axiosError = axiosErrorFactory.build(); + + vidisApi.getActivatedOffersBySchool.mockRejectedValueOnce(axiosError); + + encryptionService.decrypt.mockReturnValueOnce('un-decrypted'); + encryptionService.decrypt.mockReturnValueOnce('pw-decrypted'); + + const schoolName = 'NI_12345'; + + return { + mediaSource, + axiosError, + schoolName, + }; + }; + + it('should throw a AxiosErrorLoggable', async () => { + const { mediaSource, axiosError, schoolName } = setup(); + + const promise = service.fetchOffersForSchoolFromVidis(mediaSource, schoolName); + + await expect(promise).rejects.toThrow( + new AxiosErrorLoggable(axiosError, 'VIDIS_GET_OFFER_ITEMS_FOR_SCHOOL_FAILED') + ); + }); + }); + + describe('when an unknown error is thrown', () => { + const setup = () => { + const mediaSource = mediaSourceFactory.withVidis().build(); + + const unknownError = new Error(); + + vidisApi.getActivatedOffersBySchool.mockRejectedValueOnce(unknownError); + + encryptionService.decrypt.mockReturnValueOnce('un-decrypted'); + encryptionService.decrypt.mockReturnValueOnce('pw-decrypted'); + + const schoolName = 'NI_12345'; + + return { + mediaSource, + unknownError, + schoolName, + }; + }; + + it('should throw the unknown error', async () => { + const { mediaSource, unknownError, schoolName } = setup(); + + const promise = service.fetchOffersForSchoolFromVidis(mediaSource, schoolName); + + await expect(promise).rejects.toThrow(unknownError); + }); + }); + }); + + describe('when the media source has no basic auth config ', () => { + const setup = () => { + const mediaSource = mediaSourceFactory.build({ format: MediaSourceDataFormat.VIDIS, vidisConfig: undefined }); + + const schoolName = 'NI_12345'; + + return { + mediaSource, + schoolName, + }; + }; + + it('should throw an MediaSourceBasicAuthConfigNotFoundLoggableException', async () => { + const { mediaSource, schoolName } = setup(); + + const promise = service.fetchOffersForSchoolFromVidis(mediaSource, schoolName); + + await expect(promise).rejects.toThrow( + new MediaSourceVidisConfigNotFoundLoggableException(mediaSource.id, MediaSourceDataFormat.VIDIS) + ); + }); + }); + }); +}); diff --git a/apps/server/src/modules/school-license/service/media-school-license-fetch.service.ts b/apps/server/src/modules/school-license/service/media-school-license-fetch.service.ts new file mode 100644 index 00000000000..a1ccdc47e7f --- /dev/null +++ b/apps/server/src/modules/school-license/service/media-school-license-fetch.service.ts @@ -0,0 +1,48 @@ +import { AxiosErrorLoggable } from '@core/error/loggable'; +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; +import { Configuration, IDMBetreiberApiFactory, OfferDTO, PageOfferDTO } from '@infra/vidis-client'; +import { MediaSource, MediaSourceDataFormat } from '@modules/media-source'; +import { MediaSourceVidisConfigNotFoundLoggableException } from '@modules/media-source/loggable'; +import { Inject } from '@nestjs/common'; +import { AxiosResponse, isAxiosError } from 'axios'; + +export class MediaSchoolLicenseFetchService { + constructor(@Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService) {} + + public async fetchOffersForSchoolFromVidis(mediaSource: MediaSource, schoolName: string): Promise { + if (!mediaSource.vidisConfig) { + throw new MediaSourceVidisConfigNotFoundLoggableException(mediaSource.id, MediaSourceDataFormat.VIDIS); + } + + const { vidisConfig } = mediaSource; + const api = IDMBetreiberApiFactory( + new Configuration({ + basePath: vidisConfig.baseUrl, + }) + ); + + const decryptedUsername = this.encryptionService.decrypt(vidisConfig.username); + const decryptedPassword = this.encryptionService.decrypt(vidisConfig.password); + const basicAuthEncoded = btoa(`${decryptedUsername}:${decryptedPassword}`); + + try { + const axiosResponse: AxiosResponse = await api.getActivatedOffersBySchool( + schoolName, + undefined, + undefined, + { + headers: { Authorization: `Basic ${basicAuthEncoded}` }, + } + ); + const offerItems: OfferDTO[] = axiosResponse.data.items ?? []; + + return offerItems; + } catch (error: unknown) { + if (isAxiosError(error)) { + throw new AxiosErrorLoggable(error, 'VIDIS_GET_OFFER_ITEMS_FOR_SCHOOL_FAILED'); + } else { + throw error; + } + } + } +} diff --git a/apps/server/src/modules/school-license/service/media-school-license.service.spec.ts b/apps/server/src/modules/school-license/service/media-school-license.service.spec.ts index 8d3670120cc..c5adf5990c5 100644 --- a/apps/server/src/modules/school-license/service/media-school-license.service.spec.ts +++ b/apps/server/src/modules/school-license/service/media-school-license.service.spec.ts @@ -1,17 +1,34 @@ +import { Logger } from '@core/logger'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; +import { MediaSourceDataFormat, MediaSourceService } from '@modules/media-source'; import { mediaSourceFactory } from '@modules/media-source/testing'; +import { SchoolService } from '@modules/school'; +import { FederalStateEntityMapper } from '@modules/school/repo/mikro-orm/mapper'; +import { schoolFactory } from '@modules/school/testing'; import { ExternalToolMedium } from '@modules/tool/external-tool/domain'; import { Test, TestingModule } from '@nestjs/testing'; +import { federalStateFactory } from '@testing/factory/federal-state.factory'; import { MediaSchoolLicense } from '../domain'; +import { SchoolLicenseType } from '../enum'; +import { + BuildMediaSchoolLicenseFailedLoggable, + FederalStateAbbreviationOfSchoolNotFoundLoggableException, + SchoolNumberNotFoundLoggableException, +} from '../loggable'; import { MEDIA_SCHOOL_LICENSE_REPO, MediaSchoolLicenseRepo } from '../repo'; -import { mediaSchoolLicenseFactory } from '../testing'; +import { mediaSchoolLicenseFactory, vidisOfferFactory } from '../testing'; +import { MediaSchoolLicenseFetchService } from './media-school-license-fetch.service'; import { MediaSchoolLicenseService } from './media-school-license.service'; describe(MediaSchoolLicenseService.name, () => { let module: TestingModule; let mediaSchoolLicenseService: MediaSchoolLicenseService; let mediaSchoolLicenseRepo: DeepMocked; + let schoolService: DeepMocked; + let logger: DeepMocked; + let mediaSourceService: DeepMocked; + let mediaSchoolLicenseFetchService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -21,11 +38,31 @@ describe(MediaSchoolLicenseService.name, () => { provide: MEDIA_SCHOOL_LICENSE_REPO, useValue: createMock(), }, + { + provide: SchoolService, + useValue: createMock(), + }, + { + provide: Logger, + useValue: createMock(), + }, + { + provide: MediaSourceService, + useValue: createMock(), + }, + { + provide: MediaSchoolLicenseFetchService, + useValue: createMock(), + }, ], }).compile(); mediaSchoolLicenseService = module.get(MediaSchoolLicenseService); mediaSchoolLicenseRepo = module.get(MEDIA_SCHOOL_LICENSE_REPO); + schoolService = module.get(SchoolService); + logger = module.get(Logger); + mediaSourceService = module.get(MediaSourceService); + mediaSchoolLicenseFetchService = module.get(MediaSchoolLicenseFetchService); }); afterAll(async () => { @@ -238,4 +275,279 @@ describe(MediaSchoolLicenseService.name, () => { }); }); }); + + describe('deleteAllBySchoolAndMediaSource', () => { + describe('when school id and media source id is given', () => { + const setup = () => { + const mediaSource = mediaSourceFactory.build(); + const school = schoolFactory.build(); + + return { + mediaSource, + school, + }; + }; + + it('should delete the media school license by media source', async () => { + const { mediaSource, school } = setup(); + + await mediaSchoolLicenseService.deleteAllBySchoolAndMediaSource(school.id, mediaSource.id); + + expect(mediaSchoolLicenseRepo.deleteAllBySchoolAndMediaSource).toBeCalledWith(school.id, mediaSource.id); + }); + }); + }); + + describe('updateMediaSchoolLicenses', () => { + describe('when school id is given', () => { + const setup = () => { + const federalStateEntity = federalStateFactory.build(); + const federalState = FederalStateEntityMapper.mapToDo(federalStateEntity); + const schoolId = new ObjectId().toHexString(); + const school = schoolFactory.build({ id: schoolId, federalState, officialSchoolNumber: '00100' }); + const mediaSource = mediaSourceFactory.build(); + const offersFromMediaSource = vidisOfferFactory.buildList(3); + + schoolService.getSchoolById.mockResolvedValue(school); + mediaSourceService.findByFormat.mockResolvedValue(mediaSource); + mediaSchoolLicenseFetchService.fetchOffersForSchoolFromVidis.mockResolvedValue(offersFromMediaSource); + mediaSchoolLicenseRepo.deleteAllBySchoolAndMediaSource.mockImplementation(); + mediaSchoolLicenseRepo.saveAll.mockImplementation(); + + return { + school, + mediaSource, + offersFromMediaSource, + }; + }; + + it('should call school service', async () => { + const { school } = setup(); + + await mediaSchoolLicenseService.updateMediaSchoolLicenses(school.id); + + expect(schoolService.getSchoolById).toBeCalledWith(school.id); + }); + }); + + describe('when school without federal state was found', () => { + const setup = () => { + const schoolId = new ObjectId().toHexString(); + const school = schoolFactory.build({ id: schoolId, federalState: undefined, officialSchoolNumber: '00100' }); + + schoolService.getSchoolById.mockResolvedValue(school); + + return { + school, + }; + }; + + it('should throw FederalStateAbbreviationOfSchoolNotFoundLoggableException', async () => { + const { school } = setup(); + + await expect(mediaSchoolLicenseService.updateMediaSchoolLicenses(school.id)).rejects.toThrow( + new FederalStateAbbreviationOfSchoolNotFoundLoggableException(school.id) + ); + }); + }); + + describe('when school with federal state abbreviation and official school number was found ', () => { + const setup = () => { + const federalStateEntity = federalStateFactory.build(); + const federalState = FederalStateEntityMapper.mapToDo(federalStateEntity); + const schoolId = new ObjectId().toHexString(); + const school = schoolFactory.build({ id: schoolId, federalState, officialSchoolNumber: '00100' }); + const mediaSource = mediaSourceFactory.build(); + const offersFromMediaSource = vidisOfferFactory.buildList(3); + + schoolService.getSchoolById.mockResolvedValue(school); + mediaSourceService.findByFormat.mockResolvedValue(mediaSource); + mediaSchoolLicenseFetchService.fetchOffersForSchoolFromVidis.mockResolvedValue(offersFromMediaSource); + + return { + school, + mediaSource, + offersFromMediaSource, + }; + }; + + it('should call mediaSourceService', async () => { + const { school } = setup(); + + await mediaSchoolLicenseService.updateMediaSchoolLicenses(school.id); + + expect(mediaSourceService.findByFormat).toBeCalledWith(MediaSourceDataFormat.VIDIS); + }); + }); + + describe('when school with federal state abbreviation and without official school number was found ', () => { + const setup = () => { + const federalStateEntity = federalStateFactory.build(); + const federalState = FederalStateEntityMapper.mapToDo(federalStateEntity); + const school = schoolFactory.build({ officialSchoolNumber: undefined, federalState }); + + schoolService.getSchoolById.mockResolvedValue(school); + + return { + school, + }; + }; + + it('should throw SchoolNumberNotFoundLoggableException', async () => { + const { school } = setup(); + + await expect(mediaSchoolLicenseService.updateMediaSchoolLicenses(school.id)).rejects.toThrow( + new SchoolNumberNotFoundLoggableException(school.id) + ); + }); + }); + + describe('when media source was found ', () => { + const setup = () => { + const federalStateEntity = federalStateFactory.build(); + const federalState = FederalStateEntityMapper.mapToDo(federalStateEntity); + const school = schoolFactory.build({ officialSchoolNumber: '00100', federalState }); + const mediaSource = mediaSourceFactory.build({ format: MediaSourceDataFormat.VIDIS }); + + schoolService.getSchoolById.mockResolvedValue(school); + mediaSourceService.findByFormat.mockResolvedValue(mediaSource); + + const schoolName = `${federalState.abbreviation}_${school.officialSchoolNumber ?? ''}`; + + return { + school, + mediaSource, + schoolName, + }; + }; + + it('should call mediaSchoolLicenseFetchService', async () => { + const { school, mediaSource, schoolName } = setup(); + + await mediaSchoolLicenseService.updateMediaSchoolLicenses(school.id); + + expect(mediaSchoolLicenseFetchService.fetchOffersForSchoolFromVidis).toHaveBeenCalledWith( + mediaSource, + schoolName + ); + }); + }); + + describe('when fetching offers from media source was successful', () => { + const setup = () => { + const school = schoolFactory.build({ officialSchoolNumber: '00100' }); + const mediaSource = mediaSourceFactory.build({ format: MediaSourceDataFormat.VIDIS }); + const offersFromMediaSource = vidisOfferFactory.buildList(3); + + schoolService.getSchoolById.mockResolvedValue(school); + mediaSourceService.findByFormat.mockResolvedValue(mediaSource); + mediaSchoolLicenseFetchService.fetchOffersForSchoolFromVidis.mockResolvedValue(offersFromMediaSource); + mediaSchoolLicenseRepo.deleteAllBySchoolAndMediaSource.mockImplementation(); + mediaSchoolLicenseRepo.saveAll.mockImplementation(); + + return { + school, + mediaSource, + offersFromMediaSource, + }; + }; + + it('should delete all media school licences by school and media source', async () => { + const { school, mediaSource } = setup(); + + await mediaSchoolLicenseService.updateMediaSchoolLicenses(school.id); + + expect(mediaSchoolLicenseRepo.deleteAllBySchoolAndMediaSource).toHaveBeenCalledWith(school.id, mediaSource.id); + }); + }); + + describe('when fetching offers from media source was successful and offers have an offerId', () => { + const setup = () => { + const school = schoolFactory.build({ officialSchoolNumber: '00100' }); + const mediaSource = mediaSourceFactory.withVidis().build({ format: MediaSourceDataFormat.VIDIS }); + const offersFromMediaSource = vidisOfferFactory.buildList(5, { offerId: 12345 }); + const mediaSchoolLicenses = mediaSchoolLicenseFactory.buildList(5, { + type: SchoolLicenseType.MEDIA_LICENSE, + mediaSource, + schoolId: school.id, + mediumId: '12345', + }); + + schoolService.getSchoolById.mockResolvedValue(school); + mediaSourceService.findByFormat.mockResolvedValue(mediaSource); + mediaSchoolLicenseFetchService.fetchOffersForSchoolFromVidis.mockResolvedValue(offersFromMediaSource); + mediaSchoolLicenseRepo.deleteAllBySchoolAndMediaSource.mockImplementation(); + mediaSchoolLicenseRepo.saveAll.mockImplementation(); + + return { + school, + mediaSource, + offersFromMediaSource, + mediaSchoolLicenses, + }; + }; + + it('should save all media school licences', async () => { + const { school, mediaSchoolLicenses, mediaSource } = setup(); + + await mediaSchoolLicenseService.updateMediaSchoolLicenses(school.id); + + expect(mediaSchoolLicenseRepo.saveAll).toHaveBeenCalledWith( + expect.arrayContaining( + mediaSchoolLicenses.map((license) => + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + expect.objectContaining({ + ...license, + id: expect.any(String), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + props: expect.objectContaining({ + id: expect.any(String), + mediaSource, + mediumId: mediaSchoolLicenses[0].mediumId, + schoolId: school.id, + type: SchoolLicenseType.MEDIA_LICENSE, + }), + }) + ) + ) + ); + }); + }); + + describe('when fetching offers from media source was successful and offers have no offerId ', () => { + const setup = () => { + const school = schoolFactory.build({ officialSchoolNumber: '00100' }); + const mediaSource = mediaSourceFactory.build(); + const offersFromMediaSource = vidisOfferFactory.buildList(3, { offerId: undefined }); + + schoolService.getSchoolById.mockResolvedValue(school); + mediaSourceService.findByFormat.mockResolvedValue(mediaSource); + mediaSchoolLicenseFetchService.fetchOffersForSchoolFromVidis.mockResolvedValue(offersFromMediaSource); + mediaSchoolLicenseRepo.deleteAllBySchoolAndMediaSource.mockImplementation(); + mediaSchoolLicenseRepo.saveAll.mockImplementation(); + + return { + school, + mediaSource, + offersFromMediaSource, + }; + }; + + it('should not save media school licenses', async () => { + const { school } = setup(); + + await mediaSchoolLicenseService.updateMediaSchoolLicenses(school.id); + + expect(mediaSchoolLicenseRepo.saveAll).not.toHaveBeenCalled(); + }); + + it('should log info that medium id is missing', async () => { + const { school } = setup(); + + await mediaSchoolLicenseService.updateMediaSchoolLicenses(school.id); + + expect(logger.info).toHaveBeenCalledWith(new BuildMediaSchoolLicenseFailedLoggable()); + }); + }); + }); }); diff --git a/apps/server/src/modules/school-license/service/media-school-license.service.ts b/apps/server/src/modules/school-license/service/media-school-license.service.ts index 395aebe3609..57088868558 100644 --- a/apps/server/src/modules/school-license/service/media-school-license.service.ts +++ b/apps/server/src/modules/school-license/service/media-school-license.service.ts @@ -1,11 +1,30 @@ +import { Logger } from '@core/logger'; +import { OfferDTO } from '@infra/vidis-client'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { MediaSource, MediaSourceDataFormat, MediaSourceService } from '@modules/media-source'; +import { School, SchoolService } from '@modules/school'; import { ExternalToolMedium } from '@modules/tool/external-tool/domain/'; import { Inject } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { MediaSchoolLicense } from '../domain'; +import { SchoolLicenseType } from '../enum'; +import { + BuildMediaSchoolLicenseFailedLoggable, + FederalStateAbbreviationOfSchoolNotFoundLoggableException, + MediaSourceNotFoundLoggableException, + SchoolNumberNotFoundLoggableException, +} from '../loggable'; import { MEDIA_SCHOOL_LICENSE_REPO, MediaSchoolLicenseRepo } from '../repo'; +import { MediaSchoolLicenseFetchService } from './media-school-license-fetch.service'; export class MediaSchoolLicenseService { - constructor(@Inject(MEDIA_SCHOOL_LICENSE_REPO) private readonly mediaSchoolLicenseRepo: MediaSchoolLicenseRepo) {} + constructor( + @Inject(MEDIA_SCHOOL_LICENSE_REPO) private readonly mediaSchoolLicenseRepo: MediaSchoolLicenseRepo, + private readonly schoolService: SchoolService, + private readonly logger: Logger, + private readonly mediaSourceService: MediaSourceService, + private readonly mediaSchoolLicenseFetchService: MediaSchoolLicenseFetchService + ) {} public async deleteAllByMediaSource(mediaSourceId: EntityId): Promise { const deleteCount: number = await this.mediaSchoolLicenseRepo.deleteAllByMediaSource(mediaSourceId); @@ -36,4 +55,82 @@ export class MediaSchoolLicenseService { license.mediaSource?.sourceId === externalToolMedium.mediaSourceId ); } + + public async deleteAllBySchoolAndMediaSource(schoolId: EntityId, mediaSourceId: EntityId): Promise { + const deleteCount: number = await this.mediaSchoolLicenseRepo.deleteAllBySchoolAndMediaSource( + schoolId, + mediaSourceId + ); + + return deleteCount; + } + + public async updateMediaSchoolLicenses(schoolId: EntityId): Promise { + const school: School = await this.schoolService.getSchoolById(schoolId); + + const abbreviation: string | undefined = school.federalState?.abbreviation; + const { officialSchoolNumber } = school; + + if (!abbreviation) { + throw new FederalStateAbbreviationOfSchoolNotFoundLoggableException(schoolId); + } + + if (!officialSchoolNumber) { + throw new SchoolNumberNotFoundLoggableException(schoolId); + } + + const schoolName = `${abbreviation}_${officialSchoolNumber}`; + + const mediaSource: MediaSource | null = await this.mediaSourceService.findByFormat(MediaSourceDataFormat.VIDIS); + + if (!mediaSource) { + throw new MediaSourceNotFoundLoggableException(MediaSourceDataFormat.VIDIS); + } + + const offersFromMediaSource: OfferDTO[] = await this.mediaSchoolLicenseFetchService.fetchOffersForSchoolFromVidis( + mediaSource, + schoolName + ); + + await this.deleteAllBySchoolAndMediaSource(schoolId, mediaSource.id); + + await this.createLicenses(offersFromMediaSource, mediaSource, school); + } + + private buildMediaSchoolLicense( + school: School, + mediaSource: MediaSource, + mediumId: number | undefined + ): MediaSchoolLicense | null { + if (!mediumId) { + this.logger.info(new BuildMediaSchoolLicenseFailedLoggable()); + + return null; + } + + const license: MediaSchoolLicense = new MediaSchoolLicense({ + id: new ObjectId().toHexString(), + type: SchoolLicenseType.MEDIA_LICENSE, + schoolId: school.id, + mediaSource, + mediumId: mediumId.toString(), + }); + + return license; + } + + private async createLicenses(offers: OfferDTO[], mediaSource: MediaSource, school: School): Promise { + const newLicensesPromises: (MediaSchoolLicense | null)[] = offers.map((offer): MediaSchoolLicense | null => { + const newLicense: MediaSchoolLicense | null = this.buildMediaSchoolLicense(school, mediaSource, offer.offerId); + + return newLicense; + }); + + const newLicenses = await Promise.all(newLicensesPromises); + const filteredLicenses = newLicenses.filter((license): license is MediaSchoolLicense => license !== null); + + if (filteredLicenses.length) { + await this.saveAllMediaSchoolLicenses(filteredLicenses); + } + } } diff --git a/apps/server/src/modules/school-license/testing/index.ts b/apps/server/src/modules/school-license/testing/index.ts index a6cdca53584..69d22d095a0 100644 --- a/apps/server/src/modules/school-license/testing/index.ts +++ b/apps/server/src/modules/school-license/testing/index.ts @@ -1,2 +1,3 @@ export { mediaSchoolLicenseFactory } from './media-school-license.factory'; export { mediaSchoolLicenseEntityFactory } from './media-school-license-entity.factory'; +export { vidisOfferFactory } from './vidis-offer.factory'; diff --git a/apps/server/src/modules/school-license/testing/vidis-offer.factory.ts b/apps/server/src/modules/school-license/testing/vidis-offer.factory.ts new file mode 100644 index 00000000000..686d4aac2e5 --- /dev/null +++ b/apps/server/src/modules/school-license/testing/vidis-offer.factory.ts @@ -0,0 +1,12 @@ +import { OfferDTO } from '@infra/vidis-client'; +import { Factory } from 'fishery'; + +export const vidisOfferFactory = Factory.define(({ sequence }) => { + return { + offerId: sequence, + schoolActivations: ['00100', '00200', '00300'], + offerDescription: 'Test Description', + offerTitle: 'VIDIS Test', + offerVersion: 1, + }; +}); diff --git a/apps/server/src/modules/school-license/uc/index.ts b/apps/server/src/modules/school-license/uc/index.ts new file mode 100644 index 00000000000..834a3217a45 --- /dev/null +++ b/apps/server/src/modules/school-license/uc/index.ts @@ -0,0 +1 @@ +export { MediaSchoolLicenseUc } from './media-school-license.uc'; diff --git a/apps/server/src/modules/school-license/uc/media-school-license.uc.spec.ts b/apps/server/src/modules/school-license/uc/media-school-license.uc.spec.ts new file mode 100644 index 00000000000..a90308d1c50 --- /dev/null +++ b/apps/server/src/modules/school-license/uc/media-school-license.uc.spec.ts @@ -0,0 +1,98 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { UnauthorizedException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Permission } from '@shared/domain/interface'; +import { AuthorizationService } from '@modules/authorization'; +import { schoolEntityFactory } from '@testing/factory/school-entity.factory'; +import { userFactory } from '@testing/factory/user.factory'; +import { setupEntities } from '@testing/setup-entities'; +import { MediaSchoolLicenseService } from '../service'; +import { MediaSchoolLicenseUc } from './media-school-license.uc'; + +describe(MediaSchoolLicenseUc.name, () => { + let module: TestingModule; + let uc: MediaSchoolLicenseUc; + let mediaSchoolLicenseService: DeepMocked; + let authorizationService: DeepMocked; + + beforeAll(async () => { + await setupEntities(); + + module = await Test.createTestingModule({ + providers: [ + MediaSchoolLicenseUc, + { + provide: MediaSchoolLicenseService, + useValue: createMock(), + }, + { + provide: AuthorizationService, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(MediaSchoolLicenseUc); + mediaSchoolLicenseService = module.get(MediaSchoolLicenseService); + authorizationService = module.get(AuthorizationService); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('updateMediaSchoolLicenses', () => { + describe('when current user and schoolId is given', () => { + describe('when current user has permission', () => { + const setup = () => { + const school = schoolEntityFactory.build(); + const user = userFactory.asAdmin([Permission.MEDIA_SCHOOL_LICENSE_ADMIN]).buildWithId({ school }); + + authorizationService.getUserWithPermissions.mockResolvedValue(user); + authorizationService.checkAllPermissions.mockImplementation(() => { + void Promise.resolve(); + }); + + return { + user, + }; + }; + + it('should call mediaSchoolService', async () => { + const { user } = setup(); + + await uc.updateMediaSchoolLicenses(user.id, user.school.id); + + expect(mediaSchoolLicenseService.updateMediaSchoolLicenses).toHaveBeenCalledWith(user.school.id); + }); + }); + + describe('when current user has no permission', () => { + const setup = () => { + const school = schoolEntityFactory.build(); + const user = userFactory.buildWithId({ school }); + + authorizationService.getUserWithPermissions.mockResolvedValue(user); + authorizationService.checkAllPermissions.mockImplementation(() => { + throw new UnauthorizedException(); + }); + + return { + user, + }; + }; + + it('should throw error', async () => { + const { user } = setup(); + + await expect(uc.updateMediaSchoolLicenses(user.id, user.school.id)).rejects.toThrow(UnauthorizedException); + expect(mediaSchoolLicenseService.updateMediaSchoolLicenses).not.toHaveBeenCalled(); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/school-license/uc/media-school-license.uc.ts b/apps/server/src/modules/school-license/uc/media-school-license.uc.ts new file mode 100644 index 00000000000..7d6d551ad66 --- /dev/null +++ b/apps/server/src/modules/school-license/uc/media-school-license.uc.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common'; +import { User } from '@shared/domain/entity'; +import { Permission } from '@shared/domain/interface'; +import { EntityId } from '@shared/domain/types'; +import { AuthorizationService } from '@modules/authorization'; +import { MediaSchoolLicenseService } from '../service'; + +@Injectable() +export class MediaSchoolLicenseUc { + constructor( + private readonly mediaSchoolLicenseService: MediaSchoolLicenseService, + private readonly authorizationService: AuthorizationService + ) {} + + public async updateMediaSchoolLicenses(currentUserId: string, schoolId: EntityId): Promise { + const user: User = await this.authorizationService.getUserWithPermissions(currentUserId); + + this.authorizationService.checkAllPermissions(user, [Permission.MEDIA_SCHOOL_LICENSE_ADMIN]); + + await this.mediaSchoolLicenseService.updateMediaSchoolLicenses(schoolId); + } +} diff --git a/apps/server/src/modules/school/domain/do/federal-state.ts b/apps/server/src/modules/school/domain/do/federal-state.ts index eedc2263562..7f7cbdeb1c3 100644 --- a/apps/server/src/modules/school/domain/do/federal-state.ts +++ b/apps/server/src/modules/school/domain/do/federal-state.ts @@ -1,7 +1,11 @@ import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; import { County } from './county'; -export class FederalState extends DomainObject {} +export class FederalState extends DomainObject { + get abbreviation(): string { + return this.props.abbreviation; + } +} export interface FederalStateProps extends AuthorizableObject { name: string; diff --git a/apps/server/src/modules/school/domain/do/school.ts b/apps/server/src/modules/school/domain/do/school.ts index a335dbe9968..c3d446c57bd 100644 --- a/apps/server/src/modules/school/domain/do/school.ts +++ b/apps/server/src/modules/school/domain/do/school.ts @@ -40,6 +40,10 @@ export class School extends DomainObject { return this.props.officialSchoolNumber; } + get federalState(): FederalState | undefined { + return this.props.federalState; + } + set ldapLastSync(ldapLastSync: string | undefined) { this.props.ldapLastSync = ldapLastSync; } diff --git a/apps/server/src/modules/server/server.app.module.ts b/apps/server/src/modules/server/server.app.module.ts index d41b5eeae79..7a2cd29accb 100644 --- a/apps/server/src/modules/server/server.app.module.ts +++ b/apps/server/src/modules/server/server.app.module.ts @@ -31,7 +31,6 @@ import { PseudonymApiModule } from '@modules/pseudonym/pseudonym-api.module'; import { RocketChatModule } from '@modules/rocketchat'; import { RoomApiModule } from '@modules/room/room-api.module'; import { RosterModule } from '@modules/roster/roster.module'; -import { SchoolLicenseModule } from '@modules/school-license/school-license.module'; import { SchoolApiModule } from '@modules/school/school-api.module'; import { SharingApiModule } from '@modules/sharing/sharing-api.module'; import { ShdApiModule } from '@modules/shd/shd.api.module'; @@ -50,6 +49,7 @@ import { ConfigModule } from '@nestjs/config'; import { createConfigModuleOptions } from '@shared/common/config-module-options'; import { defaultMikroOrmOptions } from '@shared/common/defaultMikroOrmOptions'; import { ALL_ENTITIES } from '@shared/domain/entity'; +import { SchoolLicenseApiModule } from '../school-license/school-license-api.module'; import { ServerConfigController, ServerController, ServerUc } from './api'; import { SERVER_CONFIG_TOKEN, serverConfig } from './server.config'; @@ -103,7 +103,7 @@ const serverModules = [ CollaborativeTextEditorApiModule, AlertModule, UserLicenseModule, - SchoolLicenseModule, + SchoolLicenseApiModule, RoomApiModule, RosterModule, ShdApiModule, diff --git a/apps/server/src/shared/domain/interface/permission.enum.ts b/apps/server/src/shared/domain/interface/permission.enum.ts index f677991d4a9..2530c587335 100644 --- a/apps/server/src/shared/domain/interface/permission.enum.ts +++ b/apps/server/src/shared/domain/interface/permission.enum.ts @@ -78,6 +78,7 @@ export enum Permission { LESSONS_CREATE = 'LESSONS_CREATE', LESSONS_VIEW = 'LESSONS_VIEW', LINK_CREATE = 'LINK_CREATE', + MEDIA_SCHOOL_LICENSE_ADMIN = 'MEDIA_SCHOOL_LICENSE_ADMIN', NEWS_CREATE = 'NEWS_CREATE', NEWS_EDIT = 'NEWS_EDIT', NEWS_VIEW = 'NEWS_VIEW', diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index f7a693c2cc9..8e6685c0715 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -349,5 +349,14 @@ "created_at": { "$date": "2025-01-20T13:24:10.786Z" } + }, + { + "_id": { + "$oid": "679008937c51ba759ad4617a" + }, + "name": "Migration20250121203707", + "created_at": { + "$date": "2025-01-21T20:50:27.535Z" + } } ] diff --git a/backup/setup/roles.json b/backup/setup/roles.json index 8e23aeb9e50..a94befa0492 100644 --- a/backup/setup/roles.json +++ b/backup/setup/roles.json @@ -139,7 +139,8 @@ "GROUP_FULL_ADMIN", "SCHOOL_SYSTEM_EDIT", "SCHOOL_SYSTEM_VIEW", - "USER_CHANGE_OWN_NAME" + "USER_CHANGE_OWN_NAME", + "MEDIA_SCHOOL_LICENSE_ADMIN" ], "__v": 2 }, diff --git a/openapitools.json b/openapitools.json index d55223b9aa7..37c4a83372c 100644 --- a/openapitools.json +++ b/openapitools.json @@ -139,7 +139,7 @@ "skipValidateSpec": true, "enablePostProcessFile": true, "openapiNormalizer": { - "FILTER": "operationId:getActivatedOffersByRegion" + "FILTER": "operationId:getActivatedOffersByRegion|operationId:getActivatedOffersBySchool" }, "additionalProperties": { "apiPackage": "api",