Fix tests

Signed-off-by: solimant <solimant@users.noreply.github.com>
This commit is contained in:
solimant
2024-12-29 19:33:15 +00:00
parent 9d2bd50113
commit 29d8a18841
23 changed files with 336 additions and 81 deletions
@@ -69,16 +69,19 @@ export class ResponseBodyParser
const jsonContentType = Object.keys(contentTypes).find(contentType =>
contentType.split(';').includes('application/json'),
);
if (!jsonContentType) {
throw new OperationError(
this.operation,
`No application/json content type found in response for status code ${statusCode}`,
);
} else if ('$ref' in contentTypes[jsonContentType].schema) {
const eventStreamContentType = Object.keys(contentTypes).find(
contentType => contentType.split(';').includes('text/event-stream'),
);
if (jsonContentType && '$ref' in contentTypes[jsonContentType].schema) {
throw new OperationError(
this.operation,
'Reference objects are not supported',
);
} else if (!jsonContentType && !eventStreamContentType) {
throw new OperationError(
this.operation,
`No valid content type found in response for status code ${statusCode}`,
);
}
}
}
@@ -113,14 +116,20 @@ export class ResponseBodyParser
const jsonContentType = Object.keys(contentTypes ?? {}).find(contentType =>
contentType.split(';').includes('application/json'),
);
if (!jsonContentType) {
const eventStreamContentType = Object.keys(contentTypes ?? {}).find(
contentType => contentType.split(';').includes('text/event-stream'),
);
if (!jsonContentType && !eventStreamContentType) {
throw new OperationResponseError(
this.operation,
response,
'No application/json content type found in response',
'No valid content type found in response',
);
}
const schema = responseSchema.content![jsonContentType].schema;
const schema =
(jsonContentType && responseSchema.content![jsonContentType].schema) ||
(eventStreamContentType &&
responseSchema.content![eventStreamContentType].schema);
// This is a bit of type laziness. Ideally, this would be a type-narrowing function, but I wasn't able to get the types to work.
if (!schema) {
throw new OperationError(this.operation, 'No schema found in response');
@@ -145,6 +154,28 @@ export class ResponseBodyParser
}
const validate = this.ajv.compile(schema);
if (eventStreamContentType) {
const eventStreamBody = await response.text();
const jsonMatches = [...eventStreamBody.matchAll(/data:\s*(\{.*?\})/g)];
const jsonObjects = jsonMatches.map(match => {
return match[1] as any as JsonObject;
});
const invalid = jsonObjects.some(jsonObject => !validate(jsonObject));
if (invalid) {
throw new OperationParsingResponseError(
this.operation,
response,
'Response body',
validate.errors!,
);
}
return undefined;
}
const jsonBody = (await response.json()) as JsonObject;
const valid = validate(jsonBody);
if (!valid) {
@@ -44,10 +44,8 @@ components:
required: false
allowReserved: true
schema:
type: array
items:
type: integer
minimum: 0
type: integer
minimum: 0
namespace:
name: namespace
in: path
@@ -69,10 +67,8 @@ components:
required: false
allowReserved: true
schema:
type: array
items:
type: integer
minimum: 0
type: integer
minimum: 0
order:
name: order
in: query
@@ -534,7 +530,7 @@ components:
type: string
content:
type: string
format: byte
# format: byte
executable:
type: boolean
symlink:
@@ -869,9 +865,11 @@ components:
type: object
properties:
title:
$ref: '#/components/schemas/JsonValue'
type: string
# $ref: '#/components/schemas/JsonValue'
description:
$ref: '#/components/schemas/JsonValue'
type: string
# $ref: '#/components/schemas/JsonValue'
schema:
type: object
# additionalProperties: {}
@@ -971,7 +969,11 @@ components:
name:
type: string
argument:
type: object
oneOf:
- type: boolean
- type: number
- type: object
- type: string
stack:
type: string
required:
@@ -1324,6 +1326,17 @@ paths:
- id
required:
- results
'400':
description: Unsupported provider.
content:
application/json:
schema:
type: object
properties:
message:
type: string
name:
type: string
parameters:
- in: path
name: provider
@@ -21,6 +21,7 @@
// ******************************************************************
import { Action } from '../models/Action.model';
import { Autocomplete200Response } from '../models/Autocomplete200Response.model';
import { Autocomplete400Response } from '../models/Autocomplete400Response.model';
import { AutocompleteRequest } from '../models/AutocompleteRequest.model';
import { CancelTask200Response } from '../models/CancelTask200Response.model';
import { DryRun200Response } from '../models/DryRun200Response.model';
@@ -43,7 +44,7 @@ export type Autocomplete = {
resource: string;
};
body: AutocompleteRequest;
response: Autocomplete200Response;
response: Autocomplete200Response | Autocomplete400Response;
};
/**
* @public
@@ -93,8 +94,8 @@ export type ListActions = {
export type ListTasks = {
query: {
createdBy?: Array<string>;
limit?: Array<number>;
offset?: Array<number>;
limit?: number;
offset?: number;
order?: Array<string>;
status?: Array<string>;
};
@@ -0,0 +1,27 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface Autocomplete400Response {
message?: string;
name?: string;
}
@@ -17,13 +17,12 @@
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { JsonValue } from '../models/JsonValue.model';
/**
* @public
*/
export interface TemplateParameterSchemaStepsInner {
title: JsonValue;
description?: JsonValue;
title: string;
description?: string;
schema: any;
}
@@ -17,6 +17,7 @@
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { ValidationErrorArgument } from '../models/ValidationErrorArgument.model';
import { ValidationErrorPathInner } from '../models/ValidationErrorPathInner.model';
/**
@@ -30,6 +31,6 @@ export interface ValidationError {
message: string;
instance: any;
name: string;
argument: any;
argument: ValidationErrorArgument;
stack: string;
}
@@ -0,0 +1,24 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export type ValidationErrorArgument = any | boolean | number | string;
@@ -19,6 +19,7 @@ export * from '../models/ActionExample.model';
export * from '../models/ActionSchema.model';
export * from '../models/Autocomplete200Response.model';
export * from '../models/Autocomplete200ResponseResultsInner.model';
export * from '../models/Autocomplete400Response.model';
export * from '../models/AutocompleteRequest.model';
export * from '../models/CancelTask200Response.model';
export * from '../models/DryRun200Response.model';
@@ -61,4 +62,5 @@ export * from '../models/TemplateInfoEntity.model';
export * from '../models/TemplateParameterSchema.model';
export * from '../models/TemplateParameterSchemaStepsInner.model';
export * from '../models/ValidationError.model';
export * from '../models/ValidationErrorArgument.model';
export * from '../models/ValidationErrorPathInner.model';
@@ -80,11 +80,8 @@ export const spec = {
required: false,
allowReserved: true,
schema: {
type: 'array',
items: {
type: 'integer',
minimum: 0,
},
type: 'integer',
minimum: 0,
},
},
namespace: {
@@ -112,11 +109,8 @@ export const spec = {
required: false,
allowReserved: true,
schema: {
type: 'array',
items: {
type: 'integer',
minimum: 0,
},
type: 'integer',
minimum: 0,
},
},
order: {
@@ -545,7 +539,6 @@ export const spec = {
},
content: {
type: 'string',
format: 'byte',
},
executable: {
type: 'boolean',
@@ -758,10 +751,10 @@ export const spec = {
type: 'object',
properties: {
title: {
$ref: '#/components/schemas/JsonValue',
type: 'string',
},
description: {
$ref: '#/components/schemas/JsonValue',
type: 'string',
},
schema: {
type: 'object',
@@ -805,7 +798,20 @@ export const spec = {
type: 'string',
},
argument: {
type: 'object',
oneOf: [
{
type: 'boolean',
},
{
type: 'number',
},
{
type: 'object',
},
{
type: 'string',
},
],
},
stack: {
type: 'string',
@@ -1352,6 +1358,24 @@ export const spec = {
},
},
},
'400': {
description: 'Unsupported provider.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
message: {
type: 'string',
},
name: {
type: 'string',
},
},
},
},
},
},
},
parameters: [
{
@@ -16,6 +16,7 @@
import { DatabaseManager } from '@backstage/backend-defaults/database';
import { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
import ObservableImpl from 'zen-observable';
@@ -65,6 +66,24 @@ import {
import { createDefaultFilters } from '../lib/templating/filters/createDefaultFilters';
import { createRouter } from './router';
import { DatabaseTaskStore } from '../scaffolder/tasks/DatabaseTaskStore';
import { Server } from 'http';
import { wrapServer } from '@backstage/backend-openapi-utils/testUtils';
const mockAccess = jest.fn();
jest.mock('fs-extra', () => ({
...jest.requireActual('fs-extra'),
access: (...args: any[]) => mockAccess(...args),
promises: {
access: (...args: any[]) => mockAccess(...args),
},
constants: {
F_OK: 0,
W_OK: 1,
},
mkdir: jest.fn(),
remove: jest.fn(),
}));
function createDatabase(): DatabaseService {
return DatabaseManager.fromConfig(
@@ -245,7 +264,8 @@ const createTestRouter = async (
});
router.use(mockErrorHandler());
return { router, logger, taskBroker, permissions, catalog };
const app = await wrapServer(express().use(router));
return { router: app, logger, taskBroker, permissions, catalog };
};
describe('scaffolder router', () => {
@@ -102,7 +102,6 @@ import {
findTemplate,
getEntityBaseUrl,
getWorkingDirectory,
parseNumberParam,
parseStringsParam,
} from './helpers';
@@ -404,8 +403,10 @@ export async function createRouter(
description: template.metadata.description,
'ui:options': template.metadata['ui:options'],
steps: parameters.map(schema => ({
title: schema.title ?? 'Please enter the following information',
description: schema.description,
title:
(schema.title as string) ??
'Please enter the following information',
description: schema.description as string,
schema,
})),
EXPERIMENTAL_formDecorators:
@@ -587,8 +588,7 @@ export async function createRouter(
};
});
const limit = parseNumberParam(req.query.limit, 'limit');
const offset = parseNumberParam(req.query.offset, 'offset');
const { limit, offset } = req.query;
const taskPermissionFilters = await getAuthorizeConditions({
credentials: credentials,
@@ -604,8 +604,8 @@ export async function createRouter(
},
order,
pagination: {
limit: limit ? limit[0] : undefined,
offset: offset ? offset[0] : undefined,
limit,
offset,
},
permissionFilters: taskPermissionFilters,
});
@@ -105,8 +105,8 @@ export type ListActions = {};
export type ListTasks = {
query: {
createdBy?: Array<string>;
limit?: Array<number>;
offset?: Array<number>;
limit?: number;
offset?: number;
order?: Array<string>;
status?: Array<string>;
};
@@ -339,7 +339,7 @@ export class DefaultApiClient {
): Promise<TypedResponse<ListTasksResponse>> {
const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
const uriTemplate = `/v2/tasks{?createdBy*,limit*,offset*,order*,status*}`;
const uriTemplate = `/v2/tasks{?createdBy*,limit,offset,order*,status*}`;
const uri = parser.parse(uriTemplate).expand({
...request.query,
@@ -0,0 +1,27 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface Autocomplete400Response {
message?: string;
name?: string;
}
@@ -17,13 +17,12 @@
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { JsonValue } from '../models/JsonValue.model';
/**
* @public
*/
export interface TemplateParameterSchemaStepsInner {
title: JsonValue;
description?: JsonValue;
title: string;
description?: string;
schema: any;
}
@@ -17,6 +17,7 @@
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { ValidationErrorArgument } from '../models/ValidationErrorArgument.model';
import { ValidationErrorPathInner } from '../models/ValidationErrorPathInner.model';
/**
@@ -30,6 +31,6 @@ export interface ValidationError {
message: string;
instance: any;
name: string;
argument: any;
argument: ValidationErrorArgument;
stack: string;
}
@@ -0,0 +1,24 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export type ValidationErrorArgument = any | boolean | number | string;
@@ -19,6 +19,7 @@ export * from '../models/ActionExample.model';
export * from '../models/ActionSchema.model';
export * from '../models/Autocomplete200Response.model';
export * from '../models/Autocomplete200ResponseResultsInner.model';
export * from '../models/Autocomplete400Response.model';
export * from '../models/AutocompleteRequest.model';
export * from '../models/CancelTask200Response.model';
export * from '../models/DryRun200Response.model';
@@ -61,4 +62,5 @@ export * from '../models/TemplateInfoEntity.model';
export * from '../models/TemplateParameterSchema.model';
export * from '../models/TemplateParameterSchemaStepsInner.model';
export * from '../models/ValidationError.model';
export * from '../models/ValidationErrorArgument.model';
export * from '../models/ValidationErrorPathInner.model';
+2 -1
View File
@@ -61,7 +61,6 @@
},
"dependencies": {
"@backstage/catalog-model": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
@@ -73,7 +72,9 @@
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/test-utils": "workspace:^",
"cross-fetch": "^4.0.0",
"msw": "^1.0.0",
"uri-template": "^2.0.0"
}
}
@@ -14,12 +14,15 @@
* limitations under the License.
*/
import { ConfigReader } from '@backstage/core-app-api';
import { ScmIntegrations } from '@backstage/integration';
import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils';
import {
MockFetchApi,
mockApis,
registerMswTestHooks,
} from '@backstage/test-utils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { ScaffolderClient } from './api';
import { ScaffolderClient } from './ScaffolderClient';
import { fetchEventSource } from '@microsoft/fetch-event-source';
jest.mock('@microsoft/fetch-event-source');
@@ -31,6 +34,17 @@ const server = setupServer();
describe('api', () => {
registerMswTestHooks(server);
const githubIntegrationConfig = mockApis.config({
data: {
integrations: {
github: [
{
host: 'hello.com',
},
],
},
},
});
const mockBaseUrl = 'http://backstage/api';
const discoveryApi = { getBaseUrl: async () => mockBaseUrl };
@@ -43,15 +57,7 @@ describe('api', () => {
};
const scmIntegrationsApi = ScmIntegrations.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'hello.com',
},
],
},
}),
githubIntegrationConfig,
);
let apiClient: ScaffolderClient;
@@ -15,11 +15,6 @@
*/
import { parseEntityRef } from '@backstage/catalog-model';
import {
DiscoveryApi,
FetchApi,
IdentityApi,
} from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { Observable } from '@backstage/types';
@@ -50,6 +45,9 @@ import {
TaskStatus,
TypedResponse,
} from '../client/src/schema/openapi';
import { DiscoveryApi } from '../client/src/schema/openapi/generated/types/discovery';
import { FetchApi } from '../client/src/schema/openapi/generated/types/fetch';
import { IdentityApi } from './types/IdentityApi';
/**
* An API to interact with the scaffolder backend.
@@ -106,8 +104,8 @@ export class ScaffolderClient implements ScaffolderApi {
request.filterByOwnership === 'owned'
? [userEntityRef]
: undefined,
limit: request.limit ? [request.limit] : undefined,
offset: request.offset ? [request.offset] : undefined,
limit: request.limit,
offset: request.offset,
},
},
options,
@@ -0,0 +1,47 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This is a copy of the BackstageUserIdentity, to avoid importing core-plugin-api.
*/
type BackstageUserIdentity = {
/**
* The type of identity that this structure represents. In the frontend app
* this will currently always be 'user'.
*/
type: 'user';
/**
* The entityRef of the user in the catalog.
* For example User:default/sandra
*/
userEntityRef: string;
/**
* The user and group entities that the user claims ownership through
*/
ownershipEntityRefs: string[];
};
/**
* This is a partial copy of the IdentityApi, to avoid importing core-plugin-api.
*/
export type IdentityApi = {
/**
* User identity information within Backstage.
*/
getBackstageIdentity(): Promise<BackstageUserIdentity>;
};
+2
View File
@@ -87,10 +87,12 @@
"git-url-parse": "^15.0.0",
"humanize-duration": "^3.25.1",
"idb-keyval": "5.1.5",
"json-schema": "^0.4.0",
"json-schema-library": "^9.0.0",
"jszip": "^3.10.1",
"lodash": "^4.17.21",
"luxon": "^3.0.0",
"qs": "^6.9.4",
"react-resizable": "^3.0.5",
"react-use": "^17.2.4",
"react-window": "^1.8.10",
+10 -4
View File
@@ -7394,8 +7394,18 @@ __metadata:
dependencies:
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/integration": "workspace:^"
"@backstage/plugin-permission-common": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/types": "workspace:^"
"@microsoft/fetch-event-source": "npm:^2.0.1"
cross-fetch: "npm:^4.0.0"
json-schema: "npm:^0.4.0"
msw: "npm:^1.0.0"
qs: "npm:^6.9.4"
uri-template: "npm:^2.0.0"
zen-observable: "npm:^0.10.0"
languageName: unknown
linkType: soft
@@ -7536,9 +7546,7 @@ __metadata:
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/integration": "workspace:^"
"@backstage/integration-react": "workspace:^"
"@backstage/plugin-catalog": "workspace:^"
"@backstage/plugin-catalog-common": "workspace:^"
@@ -7555,7 +7563,6 @@ __metadata:
"@material-ui/core": "npm:^4.12.2"
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:4.0.0-alpha.61"
"@microsoft/fetch-event-source": "npm:^2.0.1"
"@react-hookz/web": "npm:^24.0.0"
"@rjsf/core": "npm:5.23.2"
"@rjsf/material-ui": "npm:5.23.2"
@@ -7590,7 +7597,6 @@ __metadata:
react-window: "npm:^1.8.10"
swr: "npm:^2.0.0"
yaml: "npm:^2.0.0"
zen-observable: "npm:^0.10.0"
zod: "npm:^3.22.4"
zod-to-json-schema: "npm:^3.20.4"
peerDependencies: