mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-12 02:19:43 +00:00
refactor(API): Migrate DELETE /credentials/{credentialId} and PUT /credentials/{credentialId}/transfer to the decorator pattern (#38454)
Co-authored-by: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
eb4eebaa98
commit
6307a5aaf1
+17
@@ -0,0 +1,17 @@
|
||||
import { TransferCredentialPublicDto } from '../credential-public.dto';
|
||||
|
||||
describe('TransferCredentialPublicDto', () => {
|
||||
test('accepts destinationProjectId and strips unknown keys', () => {
|
||||
const result = TransferCredentialPublicDto.safeParse({
|
||||
destinationProjectId: '1234',
|
||||
somethingElse: true,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ success: true, data: { destinationProjectId: '1234' } });
|
||||
expect(result.data).not.toHaveProperty('somethingElse');
|
||||
});
|
||||
|
||||
test('rejects a missing destinationProjectId', () => {
|
||||
expect(TransferCredentialPublicDto.safeParse({}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,12 @@ export const credentialPublicSchema = publicApiCredentialResponseSchema.extend({
|
||||
|
||||
export class CredentialPublicDto extends Z.class(credentialPublicSchema.shape) {}
|
||||
|
||||
export const credentialDeletedPublicSchema = credentialPublicSchema.extend({
|
||||
usageScope: z.enum(['project', 'instance']),
|
||||
});
|
||||
|
||||
export class DeleteCredentialPublicDto extends Z.class(credentialDeletedPublicSchema.shape) {}
|
||||
|
||||
export const credentialListItemPublicSchema = credentialPublicSchema
|
||||
.pick({ id: true, name: true, type: true, createdAt: true, updatedAt: true })
|
||||
.extend({ shared: z.array(credentialSharedPublicSchema) });
|
||||
@@ -97,3 +103,10 @@ export class UpdateCredentialPublicDto extends Z.class({
|
||||
'If false, replaces the entire data object.',
|
||||
}),
|
||||
}) {}
|
||||
|
||||
export class TransferCredentialPublicDto extends Z.class({
|
||||
destinationProjectId: z.string().openapi({
|
||||
description: 'The ID of the project to transfer the credential to.',
|
||||
example: 'VmwOO9HeTEj20kxM',
|
||||
}),
|
||||
}) {}
|
||||
|
||||
@@ -133,6 +133,8 @@ export {
|
||||
CredentialListPublicDto,
|
||||
CreateCredentialPublicDto,
|
||||
UpdateCredentialPublicDto,
|
||||
DeleteCredentialPublicDto,
|
||||
TransferCredentialPublicDto,
|
||||
credentialPublicSchema,
|
||||
} from './credentials/credential-public.dto';
|
||||
export { ListCredentialsQueryDto } from './credentials/list-credentials-query.dto';
|
||||
|
||||
@@ -136,10 +136,6 @@ export declare namespace UserRequest {
|
||||
|
||||
export declare namespace CredentialRequest {
|
||||
type Test = AuthenticatedRequest<{ id: string }, {}, {}, {}>;
|
||||
|
||||
type Delete = AuthenticatedRequest<{ id: string }, {}, {}, Record<string, string>>;
|
||||
|
||||
type Transfer = AuthenticatedRequest<{ id: string }, {}, { destinationProjectId: string }>;
|
||||
}
|
||||
|
||||
export declare namespace InsightsRequest {
|
||||
|
||||
@@ -3,8 +3,10 @@ import {
|
||||
CreateCredentialPublicDto,
|
||||
CredentialListPublicDto,
|
||||
CredentialPublicDto,
|
||||
DeleteCredentialPublicDto,
|
||||
ListCredentialsQueryDto,
|
||||
UpdateCredentialPublicDto,
|
||||
TransferCredentialPublicDto,
|
||||
} from '@n8n/api-types';
|
||||
import { LicenseState } from '@n8n/backend-common';
|
||||
import type { AuthenticatedRequest, CredentialsEntity, ICredentialsDb, User } from '@n8n/db';
|
||||
@@ -16,12 +18,14 @@ import {
|
||||
ApiSummary,
|
||||
ApiTags,
|
||||
Body,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
ProjectScope,
|
||||
PublicApiController,
|
||||
Put,
|
||||
Query,
|
||||
} from '@n8n/decorators';
|
||||
import { hasGlobalScope } from '@n8n/permissions';
|
||||
@@ -31,6 +35,7 @@ import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
||||
import { CredentialTypes } from '@/credential-types';
|
||||
import { CredentialsFinderService } from '@/credentials/credentials-finder.service';
|
||||
import { CredentialsService } from '@/credentials/credentials.service';
|
||||
import { EnterpriseCredentialsService } from '@/credentials/credentials.service.ee';
|
||||
import { CredentialsHelper } from '@/credentials-helper';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||
@@ -78,6 +83,14 @@ function toCredentialPublicDto(credential: CredentialPublicDtoSource): Credentia
|
||||
};
|
||||
}
|
||||
|
||||
/** The delete response adds `usageScope` to the standard credential fields. Carry over from legacy EOV handler. */
|
||||
function toDeleteCredentialPublicDto(credential: CredentialsEntity): DeleteCredentialPublicDto {
|
||||
return {
|
||||
...toCredentialPublicDto(credential),
|
||||
usageScope: credential.usageScope,
|
||||
};
|
||||
}
|
||||
|
||||
function toCredentialListItem(credential: CredentialsEntity) {
|
||||
return {
|
||||
id: credential.id,
|
||||
@@ -102,6 +115,7 @@ export class CredentialsPublicController {
|
||||
private readonly credentialsHelper: CredentialsHelper,
|
||||
private readonly licenseState: LicenseState,
|
||||
private readonly eventService: EventService,
|
||||
private readonly enterpriseCredentialsService: EnterpriseCredentialsService,
|
||||
) {}
|
||||
|
||||
@Get('/')
|
||||
@@ -363,4 +377,55 @@ export class CredentialsPublicController {
|
||||
|
||||
return { updatePayload, decryptedDataForDeps };
|
||||
}
|
||||
|
||||
@Delete('/:credentialId')
|
||||
@ApiKeyScope('credential:delete')
|
||||
@ProjectScope('credential:delete')
|
||||
@ApiSummary('Delete credential by ID')
|
||||
@ApiDescription(
|
||||
'Deletes a credential from your instance. You must be the owner of the credential.',
|
||||
)
|
||||
@ApiTags(['Credential'])
|
||||
@ApiResponse(200, DeleteCredentialPublicDto)
|
||||
@ApiErrorResponse(404)
|
||||
async deleteCredential(
|
||||
req: AuthenticatedRequest,
|
||||
_res: Response,
|
||||
@Param('credentialId', credentialIdParamSchema) credentialId: string,
|
||||
): Promise<DeleteCredentialPublicDto> {
|
||||
const credential = await this.credentialsFinderService.findCredentialForUser(
|
||||
credentialId,
|
||||
req.user,
|
||||
['credential:delete'],
|
||||
);
|
||||
|
||||
if (!credential) {
|
||||
throw new NotFoundError('Not Found');
|
||||
}
|
||||
|
||||
await this.credentialsService.delete(req.user, credentialId);
|
||||
|
||||
return toDeleteCredentialPublicDto(credential);
|
||||
}
|
||||
|
||||
@Put('/:credentialId/transfer')
|
||||
@ApiKeyScope('credential:move')
|
||||
@ProjectScope('credential:move')
|
||||
@ApiSummary('Transfer a credential to another project.')
|
||||
@ApiDescription('Transfer a credential to another project.')
|
||||
@ApiTags(['Credential'])
|
||||
@ApiResponse(204)
|
||||
@ApiErrorResponse(404)
|
||||
async transferCredential(
|
||||
req: AuthenticatedRequest,
|
||||
_res: Response,
|
||||
@Param('credentialId', credentialIdParamSchema) credentialId: string,
|
||||
@Body body: TransferCredentialPublicDto,
|
||||
): Promise<void> {
|
||||
await this.enterpriseCredentialsService.transferOne(
|
||||
req.user,
|
||||
credentialId,
|
||||
body.destinationProjectId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { Container } from '@n8n/di';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CredentialTypes } from '@/credential-types';
|
||||
import { CredentialsFinderService } from '@/credentials/credentials-finder.service';
|
||||
import { CredentialsService } from '@/credentials/credentials.service';
|
||||
import { EnterpriseCredentialsService } from '@/credentials/credentials.service.ee';
|
||||
import { CredentialsHelper } from '@/credentials-helper';
|
||||
import { CredentialNotFoundError } from '@/errors/credential-not-found.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
|
||||
import { sanitizeCredentials, toJsonSchema } from './credentials.utils';
|
||||
import { toJsonSchema } from './credentials.utils';
|
||||
import type { CredentialTypeRequest, CredentialRequest } from '../../../types';
|
||||
import type { PublicAPIEndpoint } from '../../shared/handler.types';
|
||||
import { publicApiScope, projectScope } from '../../shared/middlewares/global.middleware';
|
||||
|
||||
type CredentialsHandlers = {
|
||||
testCredential: PublicAPIEndpoint<CredentialRequest.Test>;
|
||||
transferCredential: PublicAPIEndpoint<CredentialRequest.Transfer>;
|
||||
deleteCredential: PublicAPIEndpoint<CredentialRequest.Delete>;
|
||||
getCredentialType: PublicAPIEndpoint<CredentialTypeRequest.Get>;
|
||||
};
|
||||
|
||||
@@ -42,42 +37,6 @@ const credentialsHandlers: CredentialsHandlers = {
|
||||
}
|
||||
},
|
||||
],
|
||||
transferCredential: [
|
||||
publicApiScope('credential:move'),
|
||||
projectScope('credential:move', 'credential'),
|
||||
async (req, res) => {
|
||||
const body = z.object({ destinationProjectId: z.string() }).parse(req.body);
|
||||
|
||||
await Container.get(EnterpriseCredentialsService).transferOne(
|
||||
req.user,
|
||||
req.params.id,
|
||||
body.destinationProjectId,
|
||||
);
|
||||
|
||||
return res.status(204).send();
|
||||
},
|
||||
],
|
||||
deleteCredential: [
|
||||
publicApiScope('credential:delete'),
|
||||
projectScope('credential:delete', 'credential'),
|
||||
async (req, res) => {
|
||||
const { id: credentialId } = req.params;
|
||||
|
||||
const credential = await Container.get(CredentialsFinderService).findCredentialForUser(
|
||||
credentialId,
|
||||
req.user,
|
||||
['credential:delete'],
|
||||
);
|
||||
|
||||
if (!credential) {
|
||||
throw new NotFoundError('Not Found');
|
||||
}
|
||||
|
||||
await Container.get(CredentialsService).delete(req.user, credentialId);
|
||||
return res.json(sanitizeCredentials(credential));
|
||||
},
|
||||
],
|
||||
|
||||
getCredentialType: [
|
||||
async (req, res) => {
|
||||
const { credentialTypeName } = req.params;
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
put:
|
||||
x-eov-operation-id: transferCredential
|
||||
x-required-scope: credential:move
|
||||
x-eov-operation-handler: v1/handlers/credentials/credentials.handler
|
||||
tags:
|
||||
- Credential
|
||||
summary: Transfer a credential to another project.
|
||||
description: Transfer a credential to another project.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/credentialId.yml'
|
||||
requestBody:
|
||||
description: Destination project for the credential transfer.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
destinationProjectId:
|
||||
type: string
|
||||
description: The ID of the project to transfer the credential to.
|
||||
required:
|
||||
- destinationProjectId
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
'400':
|
||||
$ref: '../../../../shared/spec/responses/badRequest.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
@@ -1,27 +0,0 @@
|
||||
delete:
|
||||
x-eov-operation-id: deleteCredential
|
||||
x-required-scope: credential:delete
|
||||
x-eov-operation-handler: v1/handlers/credentials/credentials.handler
|
||||
tags:
|
||||
- Credential
|
||||
summary: Delete credential by ID
|
||||
description: Deletes a credential from your instance. You must be the owner of the credentials
|
||||
operationId: deleteCredential
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
description: The credential ID that needs to be deleted
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/credential.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
operationId: deleteCredential
|
||||
tags:
|
||||
- Credential
|
||||
summary: Delete credential by ID
|
||||
description: Deletes a credential from your instance. You must be the owner of the credential.
|
||||
x-required-scope: credential:delete
|
||||
x-eov-operation-id: unreachable
|
||||
x-eov-operation-handler: v1/handlers/decorator-routed.handler
|
||||
x-decorator-routed: true
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
required: true
|
||||
description: The ID of the credential.
|
||||
name: credentialId
|
||||
in: path
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
isManaged:
|
||||
type: boolean
|
||||
isGlobal:
|
||||
type: boolean
|
||||
isResolvable:
|
||||
type: boolean
|
||||
resolvableAllowFallback:
|
||||
type: boolean
|
||||
resolverId:
|
||||
type: string
|
||||
nullable: true
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
usageScope:
|
||||
type: string
|
||||
enum:
|
||||
- project
|
||||
- instance
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- type
|
||||
- isManaged
|
||||
- isGlobal
|
||||
- isResolvable
|
||||
- resolvableAllowFallback
|
||||
- resolverId
|
||||
- createdAt
|
||||
- updatedAt
|
||||
- usageScope
|
||||
'401':
|
||||
$ref: ../../../../shared/spec/responses/unauthorized.yml
|
||||
'403':
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'404':
|
||||
$ref: ../../../../shared/spec/responses/notFound.yml
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
operationId: transferCredential
|
||||
tags:
|
||||
- Credential
|
||||
summary: Transfer a credential to another project.
|
||||
description: Transfer a credential to another project.
|
||||
x-required-scope: credential:move
|
||||
x-eov-operation-id: unreachable
|
||||
x-eov-operation-handler: v1/handlers/decorator-routed.handler
|
||||
x-decorator-routed: true
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
required: true
|
||||
description: The ID of the credential.
|
||||
name: credentialId
|
||||
in: path
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
destinationProjectId:
|
||||
type: string
|
||||
description: The ID of the project to transfer the credential to.
|
||||
example: VmwOO9HeTEj20kxM
|
||||
required:
|
||||
- destinationProjectId
|
||||
responses:
|
||||
'204':
|
||||
description: Operation successful.
|
||||
'400':
|
||||
$ref: ../../../../shared/spec/responses/badRequest.yml
|
||||
'401':
|
||||
$ref: ../../../../shared/spec/responses/unauthorized.yml
|
||||
'403':
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'404':
|
||||
$ref: ../../../../shared/spec/responses/notFound.yml
|
||||
'415':
|
||||
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml
|
||||
@@ -1,34 +0,0 @@
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
- data
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
readOnly: true
|
||||
example: R2DjclaysHbqn778
|
||||
name:
|
||||
type: string
|
||||
example: Joe's Github Credentials
|
||||
type:
|
||||
type: string
|
||||
example: githubApi
|
||||
data:
|
||||
type: object
|
||||
writeOnly: true
|
||||
example: { accessToken: 'ada612vad6fa5df4adf5a5dsf4389adsf76da7s' }
|
||||
isResolvable:
|
||||
type: boolean
|
||||
example: false
|
||||
description: Whether this credential has resolvable fields
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
example: '2022-04-29T11:02:29.842Z'
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
example: '2022-04-29T11:02:29.842Z'
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
name: id
|
||||
in: path
|
||||
description: The ID of the credential.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
@@ -13,6 +13,11 @@ paths:
|
||||
$ref: ./handlers/credentials/spec/paths/getCredential.generated.yml
|
||||
patch:
|
||||
$ref: ./handlers/credentials/spec/paths/updateCredential.generated.yml
|
||||
delete:
|
||||
$ref: ./handlers/credentials/spec/paths/deleteCredential.generated.yml
|
||||
/credentials/{credentialId}/transfer:
|
||||
put:
|
||||
$ref: ./handlers/credentials/spec/paths/transferCredential.generated.yml
|
||||
/executions:
|
||||
get:
|
||||
$ref: ./handlers/executions/spec/paths/getExecutions.generated.yml
|
||||
|
||||
@@ -93,14 +93,10 @@ paths:
|
||||
$ref: './handlers/sso-oidc/spec/paths/settings.sso.oidc.yml'
|
||||
/settings/sso/saml:
|
||||
$ref: './handlers/sso-saml/spec/paths/settings.sso.saml.yml'
|
||||
/credentials/{id}:
|
||||
$ref: './handlers/credentials/spec/paths/credentials.id.yml'
|
||||
/credentials/{id}/test:
|
||||
$ref: './handlers/credentials/spec/paths/credentials.id.test.yml'
|
||||
/credentials/schema/{credentialTypeName}:
|
||||
$ref: './handlers/credentials/spec/paths/credentials.schema.id.yml'
|
||||
/credentials/{id}/transfer:
|
||||
$ref: './handlers/credentials/spec/paths/credentials.id.transfer.yml'
|
||||
/tags/{id}:
|
||||
$ref: './handlers/tags/spec/paths/tags.id.yml'
|
||||
/workflows/{id}/{versionId}:
|
||||
|
||||
@@ -14,8 +14,6 @@ WorkflowSettings:
|
||||
$ref: './../../../handlers/workflows/spec/schemas/workflowSettings.yml'
|
||||
WorkflowList:
|
||||
$ref: './../../../handlers/workflows/spec/schemas/workflowList.yml'
|
||||
Credential:
|
||||
$ref: './../../../handlers/credentials/spec/schemas/credential.yml'
|
||||
CredentialType:
|
||||
$ref: './../../../handlers/credentials/spec/schemas/credentialType.yml'
|
||||
Audit:
|
||||
|
||||
@@ -674,10 +674,20 @@ describe('DELETE /credentials/:id', () => {
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const { name, type } = response.body;
|
||||
|
||||
expect(name).toBe(savedCredential.name);
|
||||
expect(type).toBe(savedCredential.type);
|
||||
// Exact-shape check - proves the DTO's allowlisted fields
|
||||
expect(response.body).toStrictEqual({
|
||||
id: savedCredential.id,
|
||||
name: savedCredential.name,
|
||||
type: savedCredential.type,
|
||||
isManaged: savedCredential.isManaged,
|
||||
isGlobal: savedCredential.isGlobal,
|
||||
isResolvable: savedCredential.isResolvable,
|
||||
resolvableAllowFallback: savedCredential.resolvableAllowFallback,
|
||||
resolverId: savedCredential.resolverId,
|
||||
createdAt: savedCredential.createdAt.toISOString(),
|
||||
updatedAt: savedCredential.updatedAt.toISOString(),
|
||||
usageScope: savedCredential.usageScope,
|
||||
});
|
||||
|
||||
const deletedCredential = await Container.get(CredentialsRepository).findOneBy({
|
||||
id: savedCredential.id,
|
||||
@@ -805,6 +815,20 @@ describe('DELETE /credentials/:id', () => {
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('should return 403 when the API key lacks credential:delete', async () => {
|
||||
const savedCredential = await saveCredential(dbCredential(), { user: owner });
|
||||
const agent = await makeGlobalRoleUserAgent(['credential:read']);
|
||||
|
||||
const response = await agent.delete(`/credentials/${savedCredential.id}`);
|
||||
|
||||
expect(response.statusCode).toBe(403);
|
||||
|
||||
const stillThere = await Container.get(CredentialsRepository).findOneBy({
|
||||
id: savedCredential.id,
|
||||
});
|
||||
expect(stillThere).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /credentials/:id', () => {
|
||||
@@ -1716,6 +1740,49 @@ describe('PUT /credentials/:id/transfer', () => {
|
||||
*/
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('should return 403 when the API key lacks credential:move', async () => {
|
||||
const [firstProject, secondProject] = await Promise.all([
|
||||
createTeamProject('first-project', owner),
|
||||
createTeamProject('second-project', owner),
|
||||
]);
|
||||
const credentials = await createCredentials(
|
||||
{ name: 'Test', type: 'test', data: '' },
|
||||
firstProject,
|
||||
);
|
||||
const agent = await makeGlobalRoleUserAgent(['credential:read']);
|
||||
|
||||
const response = await agent
|
||||
.put(`/credentials/${credentials.id}/transfer`)
|
||||
.send({ destinationProjectId: secondProject.id });
|
||||
|
||||
expect(response.statusCode).toBe(403);
|
||||
|
||||
const sharings = await getCredentialSharings(credentials);
|
||||
expect(sharings).toHaveLength(1);
|
||||
expect(sharings[0]).toMatchObject({ projectId: firstProject.id });
|
||||
});
|
||||
|
||||
test('should not transfer a credential the user has no access to', async () => {
|
||||
const [firstProject, secondProject] = await Promise.all([
|
||||
createTeamProject('first-project', owner),
|
||||
createTeamProject('second-project', owner),
|
||||
]);
|
||||
const credentials = await createCredentials(
|
||||
{ name: 'Test', type: 'test', data: '' },
|
||||
firstProject,
|
||||
);
|
||||
|
||||
const response = await authMemberAgent
|
||||
.put(`/credentials/${credentials.id}/transfer`)
|
||||
.send({ destinationProjectId: secondProject.id });
|
||||
|
||||
expect(response.statusCode).toBe(403);
|
||||
|
||||
const sharings = await getCredentialSharings(credentials);
|
||||
expect(sharings).toHaveLength(1);
|
||||
expect(sharings[0]).toMatchObject({ projectId: firstProject.id });
|
||||
});
|
||||
});
|
||||
|
||||
const credentialPayload = (): CredentialPayload => ({
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"PATCH /credentials/{credentialId}": {
|
||||
"status": "gap"
|
||||
},
|
||||
"DELETE /credentials/{id}": {
|
||||
"DELETE /credentials/{credentialId}": {
|
||||
"status": "covered",
|
||||
"nodeOperation": "credential:delete"
|
||||
},
|
||||
@@ -62,7 +62,7 @@
|
||||
"status": "covered",
|
||||
"nodeOperation": "credential:getSchema"
|
||||
},
|
||||
"PUT /credentials/{id}/transfer": {
|
||||
"PUT /credentials/{credentialId}/transfer": {
|
||||
"status": "gap"
|
||||
},
|
||||
"POST /credentials/{id}/test": {
|
||||
|
||||
Reference in New Issue
Block a user