Skip to content

Commit

Permalink
improve input validation with consistent error handling and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
myxmaster committed Jan 22, 2025
1 parent f7ce9de commit ca9fcc2
Show file tree
Hide file tree
Showing 5 changed files with 503 additions and 156 deletions.
10 changes: 7 additions & 3 deletions components/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface TextInputProps {
numberOfLines?: number;
style?: ViewStyle;
textInputStyle?: StyleProp<TextStyle>;
textColor?: string;
placeholderTextColor?: string;
locked?: boolean;
keyboardType?: KeyboardTypeOptions;
Expand Down Expand Up @@ -49,6 +50,7 @@ const TextInput = React.forwardRef<TextInputRN, TextInputProps>(
numberOfLines,
style,
textInputStyle,
textColor,
placeholderTextColor,
locked,
keyboardType,
Expand Down Expand Up @@ -156,9 +158,11 @@ const TextInput = React.forwardRef<TextInputRN, TextInputProps>(
style={{
...StyleSheet.flatten(textInputStyle),
...styles.input,
color: locked
? themeColor('secondaryText')
: themeColor('text')
color:
textColor ||
(locked
? themeColor('secondaryText')
: themeColor('text'))
}}
placeholderTextColor={
placeholderTextColor || themeColor('secondaryText')
Expand Down
2 changes: 0 additions & 2 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,6 @@
"general.experimental": "Experimental",
"general.defaultNodeNickname": "My Lightning Node",
"general.active": "Active",
"general.pastedInvalidData": "The pasted text contains characters we can't use here. Please check and try again.",
"general.invalidHost": "Invalid host. Please enter a valid host, or host:port.",
"restart.title": "Restart required",
"restart.msg": "ZEUS has to be restarted before the new configuration is applied.",
"restart.msg1": "Would you like to restart now?",
Expand Down
197 changes: 197 additions & 0 deletions utils/ValidationUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import ValidationUtils from './ValidationUtils';

describe('isValidHostAndPort', () => {
it('accepts valid hostname without port', () => {
expect(ValidationUtils.isValidHostAndPort('example.com')).toBe(true);
});

it('accepts valid hostname with port', () => {
expect(ValidationUtils.isValidHostAndPort('example.com:8080')).toBe(
true
);
});

it('accepts valid hostname with protocol', () => {
expect(ValidationUtils.isValidHostAndPort('https://example.com')).toBe(
true
);
expect(ValidationUtils.isValidHostAndPort('http://example.com')).toBe(
true
);
});

it('rejects invalid port numbers', () => {
expect(ValidationUtils.isValidHostAndPort('example.com:0')).toBe(false);
expect(ValidationUtils.isValidHostAndPort('example.com:65536')).toBe(
false
);
});

it('rejects invalid hostnames', () => {
expect(ValidationUtils.isValidHostAndPort('example..com')).toBe(false);
expect(ValidationUtils.isValidHostAndPort('-example.com')).toBe(false);
});
});

describe('isValidHttpsHostAndPort', () => {
it('accepts valid hostname without protocol', () => {
expect(ValidationUtils.isValidHttpsHostAndPort('example.com')).toBe(
true
);
});

it('accepts valid hostname with https protocol', () => {
expect(
ValidationUtils.isValidHttpsHostAndPort('https://example.com')
).toBe(true);
});

it('accepts valid hostname with port', () => {
expect(
ValidationUtils.isValidHttpsHostAndPort('example.com:8080')
).toBe(true);
expect(
ValidationUtils.isValidHttpsHostAndPort('https://example.com:8080')
).toBe(true);
});

it('rejects hostname with http protocol', () => {
expect(
ValidationUtils.isValidHttpsHostAndPort('http://example.com')
).toBe(false);
expect(
ValidationUtils.isValidHttpsHostAndPort('http://example.com:8080')
).toBe(false);
});

it('rejects invalid port numbers', () => {
expect(ValidationUtils.isValidHttpsHostAndPort('example.com:0')).toBe(
false
);
expect(
ValidationUtils.isValidHttpsHostAndPort('example.com:65536')
).toBe(false);
});

it('rejects invalid hostnames', () => {
expect(ValidationUtils.isValidHttpsHostAndPort('example..com')).toBe(
false
);
expect(ValidationUtils.isValidHttpsHostAndPort('-example.com')).toBe(
false
);
});
});

describe('isValidHostname', () => {
it('accepts valid hostnames', () => {
expect(ValidationUtils.isValidHostname('example.com')).toBe(true);
expect(ValidationUtils.isValidHostname('sub.example.com')).toBe(true);
expect(ValidationUtils.isValidHostname('example-domain.com')).toBe(
true
);
expect(ValidationUtils.isValidHostname('localhost')).toBe(true);
expect(ValidationUtils.isValidHostname('10.0.2.2')).toBe(true);
});

it('accepts hostnames with protocol', () => {
expect(ValidationUtils.isValidHostname('http://example.com')).toBe(
true
);
expect(ValidationUtils.isValidHostname('https://example.com')).toBe(
true
);
});

it('rejects invalid hostnames', () => {
expect(ValidationUtils.isValidHostname('example..com')).toBe(false);
expect(ValidationUtils.isValidHostname('.example.com')).toBe(false);
expect(ValidationUtils.isValidHostname('example.com:')).toBe(false);
});
});

describe('isValidPort', () => {
it('accepts valid port numbers', () => {
expect(ValidationUtils.isValidPort('80')).toBe(true);
expect(ValidationUtils.isValidPort('8080')).toBe(true);
expect(ValidationUtils.isValidPort('65535')).toBe(true);
});

it('rejects invalid port numbers', () => {
expect(ValidationUtils.isValidPort('0')).toBe(false);
expect(ValidationUtils.isValidPort('65536')).toBe(false);
expect(ValidationUtils.isValidPort('-1')).toBe(false);
});
});

describe('hasValidRuneChars', () => {
it('accepts valid rune characters', () => {
expect(ValidationUtils.hasValidRuneChars('abcDEF123-_=')).toBe(true);
expect(ValidationUtils.hasValidRuneChars('ABC123')).toBe(true);
});

it('rejects invalid rune characters', () => {
expect(ValidationUtils.hasValidRuneChars('abc!')).toBe(false);
expect(ValidationUtils.hasValidRuneChars('abc@')).toBe(false);
expect(ValidationUtils.hasValidRuneChars('abc space')).toBe(false);
});
});

describe('hasValidMacaroonChars', () => {
it('accepts valid macaroon hex characters', () => {
expect(ValidationUtils.hasValidMacaroonChars('0123456789abcdef')).toBe(
true
);
expect(ValidationUtils.hasValidMacaroonChars('ABCDEF')).toBe(true);
});

it('rejects invalid macaroon hex characters', () => {
expect(ValidationUtils.hasValidMacaroonChars('0123g')).toBe(false);
expect(ValidationUtils.hasValidMacaroonChars('abcd!')).toBe(false);
expect(ValidationUtils.hasValidMacaroonChars('0123 4567')).toBe(false);
});
});

describe('hasValidPairingPhraseCharsAndWordcount', () => {
it('accepts valid 10-word pairing phrase', () => {
expect(
ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
'cherry truth mask employ box silver mass bunker fiscal vote'
)
).toBe(true);
});

it('accepts phrase with extra spaces and normalizes', () => {
expect(
ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
' cherry truth mask employ box silver mass bunker fiscal vote '
)
).toBe(true);
});

it('rejects phrases with wrong word count', () => {
expect(
ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
'less than ten words'
)
).toBe(false);
expect(
ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
'more than ten words truth mask employ box silver mass bunker'
)
).toBe(false);
});

it('rejects phrases with invalid characters', () => {
expect(
ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
'cherry truth mask employ box silver mass bunker fiscal 123'
)
).toBe(false);
expect(
ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
'cherry truth mask employ box silver mass bunker fiscal @'
)
).toBe(false);
});
});
68 changes: 68 additions & 0 deletions utils/ValidationUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const isValidHostAndPort = (input: string): boolean => {
const urlPattern =
/^(https?:\/\/)?(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])(:\d+)?$/;
const portPattern = /:\d+$/;

if (!urlPattern.test(input)) return false;

const portMatch = input.match(portPattern);
if (portMatch) {
const port = parseInt(portMatch[0].substring(1));
if (port < 1 || port > 65535) return false;
}

return true;
};

const isValidHttpsHostAndPort = (input: string): boolean => {
const urlPattern =
/^(https:\/\/)?(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])(:\d+)?$/;
const portPattern = /:\d+$/;

if (!urlPattern.test(input)) return false;

const portMatch = input.match(portPattern);
if (portMatch) {
const port = parseInt(portMatch[0].substring(1));
if (port < 1 || port > 65535) return false;
}

return true;
};

const isValidHostname = (hostname: string): boolean => {
const urlPattern =
/^(https?:\/\/)?(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
return urlPattern.test(hostname);
};

const isValidPort = (port: string): boolean => {
const portNum = parseInt(port);
return portNum >= 1 && portNum <= 65535;
};

const hasValidRuneChars = (rune: string): boolean => {
return /^[A-Za-z0-9\-_=]+$/.test(rune);
};

const hasValidMacaroonChars = (macaroon: string): boolean => {
return /^[0-9a-fA-F]+$/.test(macaroon);
};

const hasValidPairingPhraseCharsAndWordcount = (phrase: string): boolean => {
const normalizedPhrase = phrase.trim().replace(/\s+/g, ' ');
if (!/^[a-zA-Z\s]+$/.test(normalizedPhrase)) return false;
return normalizedPhrase.split(' ').length === 10;
};

const ValidationUtils = {
isValidHostAndPort,
isValidHttpsHostAndPort,
isValidHostname,
isValidPort,
hasValidRuneChars,
hasValidMacaroonChars,
hasValidPairingPhraseCharsAndWordcount
};

export default ValidationUtils;
Loading

0 comments on commit ca9fcc2

Please sign in to comment.