mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-27 01:19:00 +02:00
feat: normalize urls in feature links (#9911)
This commit is contained in:
parent
28373f5e37
commit
20a80142d3
@ -153,6 +153,7 @@
|
|||||||
"murmurhash3js": "^3.0.1",
|
"murmurhash3js": "^3.0.1",
|
||||||
"mustache": "^4.1.0",
|
"mustache": "^4.1.0",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
|
"normalize-url": "^6.1.0",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"owasp-password-strength-test": "^1.3.0",
|
"owasp-password-strength-test": "^1.3.0",
|
||||||
"parse-database-url": "^0.3.0",
|
"parse-database-url": "^0.3.0",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createFakeFeatureLinkService } from './createFeatureLinkService';
|
import { createFakeFeatureLinkService } from './createFeatureLinkService';
|
||||||
import type { IAuditUser, IUnleashConfig } from '../../types';
|
import type { IAuditUser, IUnleashConfig } from '../../types';
|
||||||
import getLogger from '../../../test/fixtures/no-logger';
|
import getLogger from '../../../test/fixtures/no-logger';
|
||||||
import { NotFoundError } from '../../error';
|
import { BadDataError, NotFoundError } from '../../error';
|
||||||
|
|
||||||
test('create, update and delete feature link', async () => {
|
test('create, update and delete feature link', async () => {
|
||||||
const { featureLinkStore, featureLinkService } =
|
const { featureLinkStore, featureLinkService } =
|
||||||
@ -16,18 +16,22 @@ test('create, update and delete feature link', async () => {
|
|||||||
);
|
);
|
||||||
expect(link).toMatchObject({
|
expect(link).toMatchObject({
|
||||||
featureName: 'feature',
|
featureName: 'feature',
|
||||||
url: 'example.com',
|
url: 'https://example.com',
|
||||||
title: 'some title',
|
title: 'some title',
|
||||||
});
|
});
|
||||||
|
|
||||||
const newLink = await featureLinkService.updateLink(
|
const newLink = await featureLinkService.updateLink(
|
||||||
{ projectId: 'default', linkId: link.id },
|
{ projectId: 'default', linkId: link.id },
|
||||||
{ title: 'new title', url: 'example1.com', featureName: 'feature' },
|
{
|
||||||
|
title: 'new title',
|
||||||
|
url: 'https://example1.com',
|
||||||
|
featureName: 'feature',
|
||||||
|
},
|
||||||
{} as IAuditUser,
|
{} as IAuditUser,
|
||||||
);
|
);
|
||||||
expect(newLink).toMatchObject({
|
expect(newLink).toMatchObject({
|
||||||
featureName: 'feature',
|
featureName: 'feature',
|
||||||
url: 'example1.com',
|
url: 'https://example1.com',
|
||||||
title: 'new title',
|
title: 'new title',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -39,22 +43,55 @@ test('create, update and delete feature link', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('cannot delete/update non existent link', async () => {
|
test('cannot delete/update non existent link', async () => {
|
||||||
const { featureLinkStore, featureLinkService } =
|
const { featureLinkService } = createFakeFeatureLinkService({
|
||||||
createFakeFeatureLinkService({
|
|
||||||
getLogger,
|
getLogger,
|
||||||
} as unknown as IUnleashConfig);
|
} as unknown as IUnleashConfig);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
featureLinkService.updateLink(
|
featureLinkService.updateLink(
|
||||||
{ projectId: 'default', linkId: 'nonexitent' },
|
{ projectId: 'default', linkId: 'nonexistent' },
|
||||||
{ title: 'new title', url: 'example1.com', featureName: 'feature' },
|
{
|
||||||
|
title: 'new title',
|
||||||
|
url: 'https://example1.com',
|
||||||
|
featureName: 'feature',
|
||||||
|
},
|
||||||
{} as IAuditUser,
|
{} as IAuditUser,
|
||||||
),
|
),
|
||||||
).rejects.toThrow(NotFoundError);
|
).rejects.toThrow(NotFoundError);
|
||||||
await expect(
|
await expect(
|
||||||
featureLinkService.deleteLink(
|
featureLinkService.deleteLink(
|
||||||
{ projectId: 'default', linkId: 'nonexitent' },
|
{ projectId: 'default', linkId: 'nonexistent' },
|
||||||
{} as IAuditUser,
|
{} as IAuditUser,
|
||||||
),
|
),
|
||||||
).rejects.toThrow(NotFoundError);
|
).rejects.toThrow(NotFoundError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('cannot create/update invalid link', async () => {
|
||||||
|
const { featureLinkService } = createFakeFeatureLinkService({
|
||||||
|
getLogger,
|
||||||
|
} as unknown as IUnleashConfig);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
featureLinkService.createLink(
|
||||||
|
'irrelevant',
|
||||||
|
{
|
||||||
|
featureName: 'irrelevant',
|
||||||
|
url: '%example.com',
|
||||||
|
title: 'irrelevant',
|
||||||
|
},
|
||||||
|
{} as IAuditUser,
|
||||||
|
),
|
||||||
|
).rejects.toThrow(BadDataError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
featureLinkService.updateLink(
|
||||||
|
{ projectId: 'irrelevant', linkId: 'irrelevant' },
|
||||||
|
{
|
||||||
|
title: 'irrelevant',
|
||||||
|
url: '%example.com',
|
||||||
|
featureName: 'irrelevant',
|
||||||
|
},
|
||||||
|
{} as IAuditUser,
|
||||||
|
),
|
||||||
|
).rejects.toThrow(BadDataError);
|
||||||
|
});
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import type { Logger } from '../../logger';
|
import type { Logger } from '../../logger';
|
||||||
import {
|
import {
|
||||||
type IUnleashConfig,
|
|
||||||
type IAuditUser,
|
|
||||||
FeatureLinkAddedEvent,
|
FeatureLinkAddedEvent,
|
||||||
FeatureLinkUpdatedEvent,
|
|
||||||
FeatureLinkRemovedEvent,
|
FeatureLinkRemovedEvent,
|
||||||
|
FeatureLinkUpdatedEvent,
|
||||||
|
type IAuditUser,
|
||||||
|
type IUnleashConfig,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import type {
|
import type {
|
||||||
IFeatureLink,
|
IFeatureLink,
|
||||||
IFeatureLinkStore,
|
IFeatureLinkStore,
|
||||||
} from './feature-link-store-type';
|
} from './feature-link-store-type';
|
||||||
import type EventService from '../events/event-service';
|
import type EventService from '../events/event-service';
|
||||||
import { NotFoundError } from '../../error';
|
import { BadDataError, NotFoundError } from '../../error';
|
||||||
|
import normalizeUrl from 'normalize-url';
|
||||||
|
|
||||||
interface IFeatureLinkStoreObj {
|
interface IFeatureLinkStoreObj {
|
||||||
featureLinkStore: IFeatureLinkStore;
|
featureLinkStore: IFeatureLinkStore;
|
||||||
@ -36,18 +37,31 @@ export default class FeatureLinkService {
|
|||||||
return this.featureLinkStore.getAll();
|
return this.featureLinkStore.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalize(url: string) {
|
||||||
|
try {
|
||||||
|
return normalizeUrl(url, { defaultProtocol: 'https:' });
|
||||||
|
} catch (e) {
|
||||||
|
throw new BadDataError(`Invalid URL: ${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async createLink(
|
async createLink(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
newLink: Omit<IFeatureLink, 'id'>,
|
newLink: Omit<IFeatureLink, 'id'>,
|
||||||
auditUser: IAuditUser,
|
auditUser: IAuditUser,
|
||||||
): Promise<IFeatureLink> {
|
): Promise<IFeatureLink> {
|
||||||
const link = await this.featureLinkStore.insert(newLink);
|
const normalizedUrl = this.normalize(newLink.url);
|
||||||
|
|
||||||
|
const link = await this.featureLinkStore.insert({
|
||||||
|
...newLink,
|
||||||
|
url: normalizedUrl,
|
||||||
|
});
|
||||||
|
|
||||||
await this.eventService.storeEvent(
|
await this.eventService.storeEvent(
|
||||||
new FeatureLinkAddedEvent({
|
new FeatureLinkAddedEvent({
|
||||||
featureName: newLink.featureName,
|
featureName: newLink.featureName,
|
||||||
project: projectId,
|
project: projectId,
|
||||||
data: { url: newLink.url, title: newLink.title },
|
data: { url: normalizedUrl, title: newLink.title },
|
||||||
auditUser,
|
auditUser,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -60,19 +74,24 @@ export default class FeatureLinkService {
|
|||||||
updatedLink: Omit<IFeatureLink, 'id'>,
|
updatedLink: Omit<IFeatureLink, 'id'>,
|
||||||
auditUser: IAuditUser,
|
auditUser: IAuditUser,
|
||||||
): Promise<IFeatureLink> {
|
): Promise<IFeatureLink> {
|
||||||
|
const normalizedUrl = this.normalize(updatedLink.url);
|
||||||
|
|
||||||
const preData = await this.featureLinkStore.get(linkId);
|
const preData = await this.featureLinkStore.get(linkId);
|
||||||
|
|
||||||
if (!preData) {
|
if (!preData) {
|
||||||
throw new NotFoundError(`Could not find link with id ${linkId}`);
|
throw new NotFoundError(`Could not find link with id ${linkId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const link = await this.featureLinkStore.update(linkId, updatedLink);
|
const link = await this.featureLinkStore.update(linkId, {
|
||||||
|
...updatedLink,
|
||||||
|
url: normalizedUrl,
|
||||||
|
});
|
||||||
|
|
||||||
await this.eventService.storeEvent(
|
await this.eventService.storeEvent(
|
||||||
new FeatureLinkUpdatedEvent({
|
new FeatureLinkUpdatedEvent({
|
||||||
featureName: updatedLink.featureName,
|
featureName: updatedLink.featureName,
|
||||||
project: projectId,
|
project: projectId,
|
||||||
data: { url: link.url, title: link.title },
|
data: { url: normalizedUrl, title: link.title },
|
||||||
preData: { url: preData.url, title: preData.title },
|
preData: { url: preData.url, title: preData.title },
|
||||||
auditUser,
|
auditUser,
|
||||||
}),
|
}),
|
||||||
|
@ -92,14 +92,14 @@ test('should manage feature links', async () => {
|
|||||||
const links = await featureLinkStore.getAll();
|
const links = await featureLinkStore.getAll();
|
||||||
expect(links).toMatchObject([
|
expect(links).toMatchObject([
|
||||||
{
|
{
|
||||||
url: 'example.com',
|
url: 'https://example.com',
|
||||||
title: 'feature link',
|
title: 'feature link',
|
||||||
featureName: 'my_feature',
|
featureName: 'my_feature',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const { body } = await app.getProjectFeatures('default', 'my_feature');
|
const { body } = await app.getProjectFeatures('default', 'my_feature');
|
||||||
expect(body.links).toMatchObject([
|
expect(body.links).toMatchObject([
|
||||||
{ id: links[0].id, title: 'feature link', url: 'example.com' },
|
{ id: links[0].id, title: 'feature link', url: 'https://example.com' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await updatedLink('my_feature', links[0].id, {
|
await updatedLink('my_feature', links[0].id, {
|
||||||
@ -110,7 +110,7 @@ test('should manage feature links', async () => {
|
|||||||
const updatedLinks = await featureLinkStore.getAll();
|
const updatedLinks = await featureLinkStore.getAll();
|
||||||
expect(updatedLinks).toMatchObject([
|
expect(updatedLinks).toMatchObject([
|
||||||
{
|
{
|
||||||
url: 'example_updated.com',
|
url: 'https://example_updated.com',
|
||||||
title: 'feature link updated',
|
title: 'feature link updated',
|
||||||
featureName: 'my_feature',
|
featureName: 'my_feature',
|
||||||
},
|
},
|
||||||
@ -127,7 +127,7 @@ test('should manage feature links', async () => {
|
|||||||
type: 'feature-link-removed',
|
type: 'feature-link-removed',
|
||||||
data: null,
|
data: null,
|
||||||
preData: {
|
preData: {
|
||||||
url: 'example_updated.com',
|
url: 'https://example_updated.com',
|
||||||
title: 'feature link updated',
|
title: 'feature link updated',
|
||||||
},
|
},
|
||||||
featureName: 'my_feature',
|
featureName: 'my_feature',
|
||||||
@ -135,14 +135,17 @@ test('should manage feature links', async () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'feature-link-updated',
|
type: 'feature-link-updated',
|
||||||
data: { url: 'example_updated.com', title: 'feature link updated' },
|
data: {
|
||||||
preData: { url: 'example.com', title: 'feature link' },
|
url: 'https://example_updated.com',
|
||||||
|
title: 'feature link updated',
|
||||||
|
},
|
||||||
|
preData: { url: 'https://example.com', title: 'feature link' },
|
||||||
featureName: 'my_feature',
|
featureName: 'my_feature',
|
||||||
project: 'default',
|
project: 'default',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'feature-link-added',
|
type: 'feature-link-added',
|
||||||
data: { url: 'example.com', title: 'feature link' },
|
data: { url: 'https://example.com', title: 'feature link' },
|
||||||
preData: null,
|
preData: null,
|
||||||
featureName: 'my_feature',
|
featureName: 'my_feature',
|
||||||
project: 'default',
|
project: 'default',
|
||||||
|
@ -7100,6 +7100,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"normalize-url@npm:^6.1.0":
|
||||||
|
version: 6.1.0
|
||||||
|
resolution: "normalize-url@npm:6.1.0"
|
||||||
|
checksum: 10c0/95d948f9bdd2cfde91aa786d1816ae40f8262946e13700bf6628105994fe0ff361662c20af3961161c38a119dc977adeb41fc0b41b1745eb77edaaf9cb22db23
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"npm-run-path@npm:^4.0.1":
|
"npm-run-path@npm:^4.0.1":
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
resolution: "npm-run-path@npm:4.0.1"
|
resolution: "npm-run-path@npm:4.0.1"
|
||||||
@ -9457,6 +9464,7 @@ __metadata:
|
|||||||
mustache: "npm:^4.1.0"
|
mustache: "npm:^4.1.0"
|
||||||
nock: "npm:13.5.6"
|
nock: "npm:13.5.6"
|
||||||
nodemailer: "npm:^6.9.9"
|
nodemailer: "npm:^6.9.9"
|
||||||
|
normalize-url: "npm:^6.1.0"
|
||||||
openapi-enforcer: "npm:1.23.0"
|
openapi-enforcer: "npm:1.23.0"
|
||||||
openapi-types: "npm:^12.1.3"
|
openapi-types: "npm:^12.1.3"
|
||||||
owasp-password-strength-test: "npm:^1.3.0"
|
owasp-password-strength-test: "npm:^1.3.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user