Fixes - Workspace logo migration (#18035)

- Update migration command to handle case where workspace logo is
originated from workspace email and point to twenty-icons.com
- Update same logic for new workspaces
- Add feature-flag for all newly created workspaces
This commit is contained in:
Etienne
2026-02-18 16:35:48 +01:00
committed by GitHub
parent 3bd431e95d
commit 058489b5cc
11 changed files with 381 additions and 125 deletions
@@ -1,6 +1,7 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import FileType from 'file-type';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { FileFolder } from 'twenty-shared/types';
@@ -20,6 +21,7 @@ import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/service
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
@@ -27,6 +29,7 @@ import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { getImageBufferFromUrl } from 'src/utils/image';
@Command({
name: 'upgrade:1-18:migrate-workspace-pictures',
@@ -44,6 +47,7 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly applicationService: ApplicationService,
private readonly fileUrlService: FileUrlService,
private readonly secureHttpClientService: SecureHttpClientService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
@@ -135,14 +139,60 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
return;
}
const isInWorkspaceLogo = workspace.logo.startsWith(
FileFolder.WorkspaceLogo,
);
const isTwentyIconLogo = workspace.logo.includes('twenty-icons');
if (!isTwentyIconLogo && !isInWorkspaceLogo) {
this.logger.log(
`Workspace logo is not a twenty icon or a workspace logo, skipping`,
);
return;
}
this.logger.log(
`Migrating workspace logo for workspace ${workspaceId}: ${workspace.logo}`,
);
if (isInWorkspaceLogo) {
await this.migrateWorkspaceLogoFromWorkspaceFolder({
workspaceId,
logoPath: workspace.logo,
isDryRun,
workspaceCustomFlatApplication,
fileRepository,
});
}
if (isTwentyIconLogo) {
await this.migrateWorkspaceLogoFromTwentyIcons({
workspaceId,
logoUrl: workspace.logo,
isDryRun,
workspaceCustomFlatApplication,
});
}
}
private async migrateWorkspaceLogoFromWorkspaceFolder({
workspaceId,
logoPath,
isDryRun,
workspaceCustomFlatApplication,
fileRepository,
}: {
workspaceId: string;
logoPath: string;
isDryRun: boolean;
workspaceCustomFlatApplication: FlatApplication;
fileRepository: Repository<FileEntity>;
}): Promise<void> {
try {
const { type: fileExtension } = extractFolderPathFilenameAndTypeOrThrow(
workspace.logo,
);
const { type: fileExtension } =
extractFolderPathFilenameAndTypeOrThrow(logoPath);
const fileId = v4();
const newFilename = `${fileId}${isNonEmptyString(fileExtension) ? `.${fileExtension}` : ''}`;
@@ -152,7 +202,7 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
await this.fileStorageService.copyLegacy({
from: {
folderPath: `workspace-${workspaceId}`,
filename: workspace.logo,
filename: logoPath,
},
to: {
folderPath: `${workspaceId}/${workspaceCustomFlatApplication.universalIdentifier}`,
@@ -181,7 +231,7 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
}
this.logger.log(
`Migrated workspace logo for workspace ${workspaceId} (${workspace.logo} -> ${newResourcePath})`,
`Migrated workspace logo for workspace ${workspaceId} (${logoPath} -> ${newResourcePath})`,
);
} catch (error) {
this.logger.error(
@@ -191,6 +241,67 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
}
}
private async migrateWorkspaceLogoFromTwentyIcons({
workspaceId,
logoUrl,
isDryRun,
workspaceCustomFlatApplication,
}: {
workspaceId: string;
logoUrl: string;
isDryRun: boolean;
workspaceCustomFlatApplication: FlatApplication;
}): Promise<void> {
try {
const httpClient = this.secureHttpClientService.getHttpClient();
const buffer = await getImageBufferFromUrl(logoUrl, httpClient);
const type = await FileType.fromBuffer(buffer);
if (!isDefined(type) || !type.mime.startsWith('image/')) {
this.logger.warn(
`Unable to detect image type for workspace logo ${logoUrl}, skipping`,
);
return;
}
const fileId = v4();
const newFilename = `${fileId}.${type.ext}`;
const newResourcePath = `${newFilename}`;
if (!isDryRun) {
const fileEntity = await this.fileStorageService.writeFile({
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
fileFolder: FileFolder.CorePicture,
resourcePath: newResourcePath,
sourceFile: buffer,
mimeType: type.mime,
fileId,
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
await this.workspaceRepository.update(
{ id: workspaceId },
{ logoFileId: fileEntity.id },
);
}
this.logger.log(
`Migrated workspace logo from twenty-icons for workspace ${workspaceId} (${logoUrl} -> ${FileFolder.CorePicture}/${newResourcePath})`,
);
} catch (error) {
this.logger.error(
`Failed to migrate workspace logo from twenty-icons for workspace ${workspaceId}: ${error.message}`,
);
}
}
private async migrateWorkspaceMemberAvatars({
workspaceId,
isDryRun,
@@ -15,6 +15,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
@@ -51,6 +52,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
FileModule,
UserWorkspaceModule,
WorkspaceMigrationModule,
SecureHttpClientModule,
],
providers: [
MigratePersonAvatarFilesCommand,
@@ -38,13 +38,14 @@ import { EmailVerificationModule } from 'src/engine/core-modules/email-verificat
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module';
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { KeyValuePairEntity } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
import { WorkspaceSSOIdentityProviderEntity } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { TwoFactorAuthenticationMethodEntity } from 'src/engine/core-modules/two-factor-authentication/entities/two-factor-authentication-method.entity';
import { TwoFactorAuthenticationModule } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication.module';
@@ -117,6 +118,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
ApplicationModule,
WorkspaceCacheModule,
SecureHttpClientModule,
FileModule,
],
controllers: [
GoogleAuthController,
@@ -97,6 +97,9 @@ const createSignInUpServiceForTests = () => {
{
createWorkspaceCustomApplication: jest.fn(),
} as any,
{
uploadWorkspaceLogoFromUrl: jest.fn(),
} as any,
{
createQueryRunner: jest.fn(() => queryRunnerMock),
} as any,
@@ -3,6 +3,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { TWENTY_ICONS_BASE_URL } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { Repository, type DataSource, type QueryRunner } from 'typeorm';
import { v4 } from 'uuid';
@@ -27,6 +28,7 @@ import {
type SignInUpNewUserPayload,
} from 'src/engine/core-modules/auth/types/signInUp.type';
import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service';
import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
@@ -62,6 +64,7 @@ export class SignInUpService {
private readonly metricsService: MetricsService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly applicationService: ApplicationService,
private readonly fileCorePictureService: FileCorePictureService,
@InjectDataSource()
private readonly dataSource: DataSource,
) {}
@@ -463,21 +466,7 @@ export class SignInUpService {
const shouldGrantServerAdmin = !(await this.hasServerAdmin());
const logoUrl = `${TWENTY_ICONS_BASE_URL}/${getDomainNameByEmail(email)}`;
const isLogoUrlValid = async () => {
try {
const httpClient = this.secureHttpClientService.getHttpClient();
const response = await httpClient.get(logoUrl, { timeout: 600 });
return response.status === 200;
} catch {
return false;
}
};
const isWorkEmailFound = isWorkEmail(email);
const logo =
isWorkEmailFound && (await isLogoUrlValid()) ? logoUrl : undefined;
const workspaceId = v4();
const workspaceCustomApplicationId = v4();
@@ -496,7 +485,6 @@ export class SignInUpService {
displayName: '',
inviteHash: v4(),
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
logo,
});
const workspace = await queryRunner.manager.save(
@@ -513,6 +501,26 @@ export class SignInUpService {
queryRunner,
);
if (isWorkEmailFound) {
const logoUrl = `${TWENTY_ICONS_BASE_URL}/${getDomainNameByEmail(email)}`;
const logoFile =
await this.fileCorePictureService.uploadWorkspaceLogoFromUrl({
imageUrl: logoUrl,
workspaceId,
applicationUniversalIdentifier:
customApplication.universalIdentifier,
queryRunner,
});
if (isDefined(logoFile)) {
await queryRunner.manager.update(
WorkspaceEntity,
{ id: workspaceId },
{ logoFileId: logoFile.id },
);
}
}
const isExistingUser = userData.type === 'existingUser';
const user = isExistingUser
? userData.existingUser
@@ -16,11 +16,15 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
@UseGuards(WorkspaceAuthGuard)
@UsePipes(ResolverValidationPipe)
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
@UseFilters(
PermissionsGraphqlApiExceptionFilter,
PreventNestToAutoLogGraphqlErrorsFilter,
)
@MetadataResolver()
export class FileCorePictureResolver {
constructor(
@@ -64,7 +64,7 @@ export class FileCorePictureService {
const savedFile = await this.fileStorageService.writeFile({
sourceFile: sanitizedFile,
resourcePath: `${FileFolder.CorePicture}/${finalName}`,
resourcePath: finalName,
mimeType,
fileFolder: FileFolder.CorePicture,
applicationUniversalIdentifier: universalIdentifier,
@@ -182,6 +182,25 @@ export class FileCorePictureService {
});
}
private async fetchImageBufferFromUrl(
imageUrl: string,
): Promise<{ buffer: Buffer; extension: string } | undefined> {
try {
const httpClient = this.secureHttpClientService.getHttpClient();
const buffer = await getImageBufferFromUrl(imageUrl, httpClient);
const type = await FileType.fromBuffer(buffer);
if (!isDefined(type) || !type.mime.startsWith('image/')) {
return undefined;
}
return { buffer, extension: type.ext };
} catch {
return undefined;
}
}
async uploadWorkspaceMemberProfilePictureFromUrl({
imageUrl,
workspaceId,
@@ -193,18 +212,41 @@ export class FileCorePictureService {
applicationUniversalIdentifier?: string;
queryRunner?: QueryRunner;
}): Promise<FileWithSignedUrlDto | undefined> {
const httpClient = this.secureHttpClientService.getHttpClient();
const buffer = await getImageBufferFromUrl(imageUrl, httpClient);
const imageData = await this.fetchImageBufferFromUrl(imageUrl);
const type = await FileType.fromBuffer(buffer);
if (!isDefined(type) || !type.mime.startsWith('image/')) {
return;
if (!isDefined(imageData)) {
return undefined;
}
return this.uploadWorkspaceMemberProfilePicture({
file: buffer,
filename: `avatar.${type.ext}`,
file: imageData.buffer,
filename: `avatar.${imageData.extension}`,
workspaceId,
applicationUniversalIdentifier,
queryRunner,
});
}
async uploadWorkspaceLogoFromUrl({
imageUrl,
workspaceId,
applicationUniversalIdentifier,
queryRunner,
}: {
imageUrl: string;
workspaceId: string;
applicationUniversalIdentifier?: string;
queryRunner?: QueryRunner;
}): Promise<FileEntity | undefined> {
const imageData = await this.fetchImageBufferFromUrl(imageUrl);
if (!isDefined(imageData)) {
return undefined;
}
return this.uploadCorePicture({
file: imageData.buffer,
filename: `logo.${imageData.extension}`,
workspaceId,
applicationUniversalIdentifier,
queryRunner,
@@ -111,6 +111,21 @@ export const seedFeatureFlags = async ({
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_CORE_PICTURE_MIGRATED,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_OTHER_FILE_MIGRATED,
workspaceId: workspaceId,
value: true,
},
])
.execute();
};
@@ -5,4 +5,7 @@ export const DEFAULT_FEATURE_FLAGS = [
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
FeatureFlagKey.IS_CORE_PICTURE_MIGRATED,
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
FeatureFlagKey.IS_OTHER_FILE_MIGRATED,
] as const satisfies FeatureFlagKey[];
@@ -1,10 +1,20 @@
import { gql } from 'graphql-tag';
import request from 'supertest';
import { makeMetadataAPIRequestWithFileUpload } from 'test/integration/metadata/suites/utils/make-metadata-api-request-with-file-upload.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
const uploadWorkspaceLogoMutation = gql`
mutation UploadWorkspaceLogo($file: Upload!) {
uploadWorkspaceLogo(file: $file) {
id
url
}
}
`;
const client = request(`http://localhost:${APP_PORT}`);
describe('Security permissions', () => {
@@ -506,62 +516,85 @@ describe('Security permissions', () => {
});
describe('logo update', () => {
beforeAll(() => {
jest.useRealTimers();
});
afterAll(() => {
jest.useFakeTimers();
});
it('should update workspace logo when user has workspace settings permission', async () => {
const queryData = {
query: `
mutation updateWorkspace {
updateWorkspace(data: { logo: "new-logo" }) {
id
const testImageBuffer = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
'base64',
);
const uploadResponse = await makeMetadataAPIRequestWithFileUpload(
{
query: uploadWorkspaceLogoMutation,
variables: { file: null },
},
{
field: 'file',
buffer: testImageBuffer,
filename: 'test-logo.png',
contentType: 'image/png',
},
APPLE_JANE_ADMIN_ACCESS_TOKEN,
);
expect(uploadResponse.status).toBe(200);
expect(uploadResponse.body.errors).toBeUndefined();
expect(uploadResponse.body.data).toBeDefined();
expect(uploadResponse.body.data.uploadWorkspaceLogo).toBeDefined();
expect(uploadResponse.body.data.uploadWorkspaceLogo.id).toBeDefined();
expect(uploadResponse.body.data.uploadWorkspaceLogo.url).toBeDefined();
const getWorkspaceQuery = gql`
query GetWorkspace {
currentWorkspace {
logo
}
}
`,
};
`;
return client
.post('/metadata')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.updateWorkspace;
const workspaceResponse = await makeMetadataAPIRequest({
query: getWorkspaceQuery,
});
expect(data).toBeDefined();
expect(data.logo).toContain('new-logo');
});
expect(workspaceResponse.body.data.currentWorkspace.logo).toBeDefined();
});
it('should throw a permission error when user does not have permission (member role)', async () => {
const queryData = {
query: `
mutation updateWorkspace {
updateWorkspace(data: { logo: "another-new-logo" }) {
id
logo
}
}
`,
};
const testImageBuffer = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
'base64',
);
await client
.post('/metadata')
.set('Authorization', `Bearer ${APPLE_JONY_MEMBER_ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeNull();
expect(res.body.errors).toBeDefined();
expect(res.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(res.body.errors[0].extensions.code).toBe(
ErrorCode.FORBIDDEN,
);
});
const response = await makeMetadataAPIRequestWithFileUpload(
{
query: uploadWorkspaceLogoMutation,
variables: { file: null },
},
{
field: 'file',
buffer: testImageBuffer,
filename: 'test-logo.png',
contentType: 'image/png',
},
APPLE_JONY_MEMBER_ACCESS_TOKEN,
);
expect(response.status).toBe(200);
expect(response.body.data).toBeNull();
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(response.body.errors[0].extensions.code).toBe(
ErrorCode.FORBIDDEN,
);
});
});
});
@@ -1,5 +1,6 @@
import gql from 'graphql-tag';
import request from 'supertest';
import { makeMetadataAPIRequestWithFileUpload } from 'test/integration/metadata/suites/utils/make-metadata-api-request-with-file-upload.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
@@ -7,6 +8,15 @@ import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/featu
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
const uploadWorkspaceLogoMutation = gql`
mutation UploadWorkspaceLogo($file: Upload!) {
uploadWorkspaceLogo(file: $file) {
id
url
}
}
`;
const client = request(`http://localhost:${APP_PORT}`);
describe('workspace permissions', () => {
@@ -266,62 +276,85 @@ describe('workspace permissions', () => {
});
describe('logo update', () => {
beforeAll(() => {
jest.useRealTimers();
});
afterAll(() => {
jest.useFakeTimers();
});
it('should update workspace logo when user has workspace settings permission', async () => {
const queryData = {
query: `
mutation updateWorkspace {
updateWorkspace(data: { logo: "new-logo" }) {
id
logo
const testImageBuffer = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
'base64',
);
const uploadResponse = await makeMetadataAPIRequestWithFileUpload(
{
query: uploadWorkspaceLogoMutation,
variables: { file: null },
},
{
field: 'file',
buffer: testImageBuffer,
filename: 'test-logo.png',
contentType: 'image/png',
},
APPLE_JANE_ADMIN_ACCESS_TOKEN,
);
expect(uploadResponse.status).toBe(200);
expect(uploadResponse.body.errors).toBeUndefined();
expect(uploadResponse.body.data).toBeDefined();
expect(uploadResponse.body.data.uploadWorkspaceLogo).toBeDefined();
expect(uploadResponse.body.data.uploadWorkspaceLogo.id).toBeDefined();
expect(uploadResponse.body.data.uploadWorkspaceLogo.url).toBeDefined();
const getWorkspaceQuery = gql`
query GetWorkspace {
currentWorkspace {
logo
}
}
}
`,
};
`;
return client
.post('/metadata')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.updateWorkspace;
const workspaceResponse = await makeMetadataAPIRequest({
query: getWorkspaceQuery,
});
expect(data).toBeDefined();
expect(data.logo).toContain('new-logo');
});
expect(workspaceResponse.body.data.currentWorkspace.logo).toBeDefined();
});
it('should throw a permission error when user does not have permission (member role)', async () => {
const queryData = {
query: `
mutation updateWorkspace {
updateWorkspace(data: { logo: "another-new-logo" }) {
id
logo
}
}
`,
};
const testImageBuffer = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
'base64',
);
await client
.post('/metadata')
.set('Authorization', `Bearer ${APPLE_JONY_MEMBER_ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeNull();
expect(res.body.errors).toBeDefined();
expect(res.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(res.body.errors[0].extensions.code).toBe(
ErrorCode.FORBIDDEN,
);
});
const response = await makeMetadataAPIRequestWithFileUpload(
{
query: uploadWorkspaceLogoMutation,
variables: { file: null },
},
{
field: 'file',
buffer: testImageBuffer,
filename: 'test-logo.png',
contentType: 'image/png',
},
APPLE_JONY_MEMBER_ACCESS_TOKEN,
);
expect(response.status).toBe(200);
expect(response.body.data).toBeNull();
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(response.body.errors[0].extensions.code).toBe(
ErrorCode.FORBIDDEN,
);
});
});
});