Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion backend/src/validators/user.validator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
import { z } from 'zod';

/**
* Stellar Ed25519 account IDs are base32 strings: a `G` version byte prefix
* followed by 55 characters from the RFC 4648 base32 alphabet, 56 in total.
*
* This is a format check only — it deliberately does not verify the trailing
* CRC16 checksum, so callers must not treat a match as proof the key exists
* or was typed correctly.
*/
export const STELLAR_PUBLIC_KEY_REGEX = /^G[A-Z2-7]{55}$/;

export const STELLAR_PUBLIC_KEY_ERROR =
'Invalid Stellar public key format: expected 56 base32 characters starting with "G"';

/**
* Reusable schema for a Stellar account public key.
*
* Rejects malformed keys at the validation layer so they never reach the
* controller or repository, where they would surface as opaque lookup misses
* or be stored as-is.
*/
export const stellarPublicKeySchema = z
.string({ message: 'publicKey is required and must be a string' })
.regex(STELLAR_PUBLIC_KEY_REGEX, STELLAR_PUBLIC_KEY_ERROR);

export const registerUserSchema = z.object({
publicKey: z.string().min(50, 'Invalid Stellar public key').regex(/^G[A-Z2-7]{55}$/, 'Invalid Stellar public key format'),
publicKey: stellarPublicKeySchema,
});

export type RegisterUserInput = z.infer<typeof registerUserSchema>;
68 changes: 68 additions & 0 deletions backend/tests/user.validator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, it, expect } from 'vitest';
import { ZodError } from 'zod';
import {
registerUserSchema,
STELLAR_PUBLIC_KEY_ERROR,
} from '../src/validators/user.validator.js';

const VALID_KEY = 'GD2XP6FNWL6IWULVMPNA2RV2T7GLCJHK3RH75GBCY7TSVIWDITJN4FXJ';

describe('User Validator', () => {
it('should accept a well-formed Stellar public key', () => {
const result = registerUserSchema.safeParse({ publicKey: VALID_KEY });
expect(result.success).toBe(true);
});

it('should reject a malformed public key with a descriptive message', () => {
const result = registerUserSchema.safeParse({ publicKey: 'not-a-stellar-key' });

expect(result.success).toBe(false);
expect(result.error?.issues).toHaveLength(1);
expect(result.error?.issues[0]?.path).toEqual(['publicKey']);
expect(result.error?.issues[0]?.message).toBe(STELLAR_PUBLIC_KEY_ERROR);
});

it.each([
['too short', 'GD2XP6FNWL6IWULV'],
['too long', `${VALID_KEY}AAAA`],
['using a wrong version prefix', `S${VALID_KEY.slice(1)}`],
['lowercase', VALID_KEY.toLowerCase()],
['outside the base32 alphabet', `${VALID_KEY.slice(0, 55)}1`],
['an empty string', ''],
['whitespace padded', ` ${VALID_KEY} `],
])('should reject a key that is %s', (_label, publicKey) => {
const result = registerUserSchema.safeParse({ publicKey });

expect(result.success).toBe(false);
expect(result.error?.issues[0]?.message).toBe(STELLAR_PUBLIC_KEY_ERROR);
});

it.each([
['missing', undefined],
['a number', 12345],
['null', null],
])('should reject a publicKey that is %s', (_label, publicKey) => {
const result = registerUserSchema.safeParse({ publicKey });

expect(result.success).toBe(false);
expect(result.error?.issues[0]?.message).toBe(
'publicKey is required and must be a string',
);
});

// `registerUser` calls `.parse`, so a malformed key throws before any Prisma
// access. The global error middleware turns that ZodError into a 400 carrying
// the message asserted above (see tests/error.middleware.test.ts).
it('should throw a ZodError so the request fails before the controller body runs', () => {
const parse = () => registerUserSchema.parse({ publicKey: 'not-a-stellar-key' });

expect(parse).toThrow(ZodError);

try {
parse();
expect.unreachable('parse should have thrown');
} catch (error) {
expect((error as ZodError).issues[0]?.message).toBe(STELLAR_PUBLIC_KEY_ERROR);
}
});
});
Loading