This commit is contained in:
2025-12-11 13:50:38 +01:00
parent e4c1be628d
commit c7e5de40d6
212 changed files with 2965 additions and 763 deletions

View File

@@ -0,0 +1,36 @@
import { MediaUrl } from './MediaUrl';
describe('MediaUrl', () => {
it('creates from valid http/https URLs', () => {
expect(MediaUrl.create('http://example.com').value).toBe('http://example.com');
expect(MediaUrl.create('https://example.com/path').value).toBe('https://example.com/path');
});
it('creates from data URIs', () => {
const url = 'data:image/jpeg;base64,AAA';
expect(MediaUrl.create(url).value).toBe(url);
});
it('creates from root-relative paths', () => {
expect(MediaUrl.create('/images/avatar.png').value).toBe('/images/avatar.png');
});
it('rejects empty or whitespace URLs', () => {
expect(() => MediaUrl.create('')).toThrow();
expect(() => MediaUrl.create(' ')).toThrow();
});
it('rejects unsupported schemes', () => {
expect(() => MediaUrl.create('ftp://example.com/file')).toThrow();
expect(() => MediaUrl.create('mailto:user@example.com')).toThrow();
});
it('implements value-based equality', () => {
const a = MediaUrl.create('https://example.com/a.png');
const b = MediaUrl.create('https://example.com/a.png');
const c = MediaUrl.create('https://example.com/b.png');
expect(a.equals(b)).toBe(true);
expect(a.equals(c)).toBe(false);
});
});

View File

@@ -0,0 +1,46 @@
import type { IValueObject } from '@gridpilot/shared/domain';
/**
* Value Object: MediaUrl
*
* Represents a validated media URL used for user-uploaded or generated assets.
* For now this is a conservative wrapper around strings with basic invariants:
* - non-empty
* - must start with "http", "https", "data:", or "/"
*/
export interface MediaUrlProps {
value: string;
}
export class MediaUrl implements IValueObject<MediaUrlProps> {
public readonly props: MediaUrlProps;
private constructor(value: string) {
this.props = { value };
}
static create(raw: string): MediaUrl {
const value = raw?.trim();
if (!value) {
throw new Error('Media URL cannot be empty');
}
const allowedPrefixes = ['http://', 'https://', 'data:', '/'];
const isAllowed = allowedPrefixes.some((prefix) => value.startsWith(prefix));
if (!isAllowed) {
throw new Error('Media URL must be http(s), data URI, or root-relative path');
}
return new MediaUrl(value);
}
get value(): string {
return this.props.value;
}
equals(other: IValueObject<MediaUrlProps>): boolean {
return this.props.value === other.props.value;
}
}