diff --git a/Backend/package.json b/Backend/package.json index f5c30d53..143af2d4 100644 --- a/Backend/package.json +++ b/Backend/package.json @@ -87,6 +87,12 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, + "transformIgnorePatterns": [ + "node_modules/(?!(@stellar/stellar-sdk|@noble)/)" + ], + "moduleNameMapper": { + "^src/(.*)$": "/$1" + }, "collectCoverageFrom": [ "**/*.(t|j)s" ], diff --git a/Backend/src/common/validators/index.ts b/Backend/src/common/validators/index.ts new file mode 100644 index 00000000..35e4316d --- /dev/null +++ b/Backend/src/common/validators/index.ts @@ -0,0 +1 @@ +export { IsStellarAddress } from './stellar-address.validator'; diff --git a/Backend/src/common/validators/stellar-address.validator.spec.ts b/Backend/src/common/validators/stellar-address.validator.spec.ts new file mode 100644 index 00000000..79e01a58 --- /dev/null +++ b/Backend/src/common/validators/stellar-address.validator.spec.ts @@ -0,0 +1,54 @@ +import { validate } from 'class-validator'; +import { IsOptional } from 'class-validator'; + +// Mock @stellar/stellar-sdk before importing the validator +jest.mock('@stellar/stellar-sdk', () => ({ + StrKey: { + isValidEd25519PublicKey: (value: string) => + typeof value === 'string' && value.length === 55 && /^G[A-Z2-7]{54}$/.test(value), + }, +})); + +import { IsStellarAddress } from './stellar-address.validator'; + +class TestDto { + @IsOptional() + @IsStellarAddress() + authorAddress?: string; +} + +const VALID_ADDRESS = 'GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN'; + +describe('IsStellarAddress', () => { + it('passes with a valid Stellar public key', async () => { + const dto = Object.assign(new TestDto(), { authorAddress: VALID_ADDRESS }); + const errors = await validate(dto); + expect(errors).toHaveLength(0); + }); + + it('passes when authorAddress is absent (anonymous post)', async () => { + const errors = await validate(new TestDto()); + expect(errors).toHaveLength(0); + }); + + it('fails with an invalid string', async () => { + const dto = Object.assign(new TestDto(), { authorAddress: 'not-a-stellar-key' }); + const errors = await validate(dto); + expect(errors).toHaveLength(1); + expect(errors[0].constraints?.isStellarAddress).toBe( + 'authorAddress must be a valid Stellar public key', + ); + }); + + it('fails with an address that does not start with G', async () => { + const dto = Object.assign(new TestDto(), { authorAddress: 'XAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN' }); + const errors = await validate(dto); + expect(errors).toHaveLength(1); + }); + + it('fails with an empty string', async () => { + const dto = Object.assign(new TestDto(), { authorAddress: '' }); + const errors = await validate(dto); + expect(errors).toHaveLength(1); + }); +}); diff --git a/Backend/src/common/validators/stellar-address.validator.ts b/Backend/src/common/validators/stellar-address.validator.ts new file mode 100644 index 00000000..fedd47b2 --- /dev/null +++ b/Backend/src/common/validators/stellar-address.validator.ts @@ -0,0 +1,21 @@ +import { registerDecorator, ValidationOptions } from 'class-validator'; +import { StrKey } from '@stellar/stellar-sdk'; + +export function IsStellarAddress(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isStellarAddress', + target: object.constructor, + propertyName, + options: validationOptions, + validator: { + validate(value: unknown) { + return typeof value === 'string' && StrKey.isValidEd25519PublicKey(value); + }, + defaultMessage() { + return 'authorAddress must be a valid Stellar public key'; + }, + }, + }); + }; +} diff --git a/Backend/src/gists/dto/create-gist.dto.ts b/Backend/src/gists/dto/create-gist.dto.ts index be2dc003..2bbc261b 100644 --- a/Backend/src/gists/dto/create-gist.dto.ts +++ b/Backend/src/gists/dto/create-gist.dto.ts @@ -20,10 +20,12 @@ export class CreateGistDto { lon: number; @ApiPropertyOptional({ - description: 'Optional Stellar address of the author', - example: 'GABC...XYZ', + description: 'Optional Stellar public key of the author (Ed25519, starts with G, 56 chars)', + example: 'GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN', }) @IsOptional() + @IsStellarAddress() + authorAddress?: string; @IsString() @MaxLength(80) author?: string;