@@ -3,6 +3,7 @@ import { http, HttpResponse } from "msw";
33import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
44/* eslint-enable workers-sdk/no-vitest-import-expect */
55import { ServiceType } from "../vpc/index" ;
6+ import { validateHostname , validateRequest } from "../vpc/validation" ;
67import { endEventLoop } from "./helpers/end-event-loop" ;
78import { mockAccountId , mockApiToken } from "./helpers/mock-account-id" ;
89import { mockConsoleMethods } from "./helpers/mock-console" ;
@@ -15,6 +16,7 @@ import type {
1516 ConnectivityService ,
1617 ConnectivityServiceRequest ,
1718} from "../vpc/index" ;
19+ import type { ServiceArgs } from "../vpc/validation" ;
1820
1921describe ( "vpc help" , ( ) => {
2022 const std = mockConsoleMethods ( ) ;
@@ -325,6 +327,155 @@ describe("vpc service commands", () => {
325327 } ) ;
326328} ) ;
327329
330+ describe ( "hostname validation" , ( ) => {
331+ it ( "should accept valid hostnames" , ( ) => {
332+ expect ( ( ) => validateHostname ( "api.example.com" ) ) . not . toThrow ( ) ;
333+ expect ( ( ) => validateHostname ( "localhost" ) ) . not . toThrow ( ) ;
334+ expect ( ( ) => validateHostname ( "my-service.internal.local" ) ) . not . toThrow ( ) ;
335+ expect ( ( ) => validateHostname ( "sub.domain.example.co.uk" ) ) . not . toThrow ( ) ;
336+ } ) ;
337+
338+ it ( "should reject empty hostname" , ( ) => {
339+ expect ( ( ) => validateHostname ( "" ) ) . toThrow ( "Hostname cannot be empty." ) ;
340+ expect ( ( ) => validateHostname ( " " ) ) . toThrow ( "Hostname cannot be empty." ) ;
341+ } ) ;
342+
343+ it ( "should reject hostname exceeding 253 characters" , ( ) => {
344+ const longHostname = "a" . repeat ( 254 ) ;
345+ expect ( ( ) => validateHostname ( longHostname ) ) . toThrow (
346+ "Hostname is too long. Maximum length is 253 characters."
347+ ) ;
348+ } ) ;
349+
350+ it ( "should accept hostname at exactly 253 characters" , ( ) => {
351+ const label = "a" . repeat ( 63 ) ;
352+ const hostname = `${ label } .${ label } .${ label } .${ label . slice ( 0 , 61 ) } ` ;
353+ expect ( hostname . length ) . toBe ( 253 ) ;
354+ expect ( ( ) => validateHostname ( hostname ) ) . not . toThrow ( ) ;
355+ } ) ;
356+
357+ it ( "should reject hostname with URL scheme" , ( ) => {
358+ expect ( ( ) => validateHostname ( "https://example.com" ) ) . toThrow (
359+ "Hostname must not include a URL scheme"
360+ ) ;
361+ expect ( ( ) => validateHostname ( "http://example.com" ) ) . toThrow (
362+ "Hostname must not include a URL scheme"
363+ ) ;
364+ } ) ;
365+
366+ it ( "should reject hostname with path" , ( ) => {
367+ expect ( ( ) => validateHostname ( "example.com/path" ) ) . toThrow (
368+ "Hostname must not include a path"
369+ ) ;
370+ } ) ;
371+
372+ it ( "should reject bare IPv4 address" , ( ) => {
373+ expect ( ( ) => validateHostname ( "192.168.1.1" ) ) . toThrow (
374+ "Hostname must not be an IP address. Use --ipv4 or --ipv6 instead."
375+ ) ;
376+ expect ( ( ) => validateHostname ( "10.0.0.1" ) ) . toThrow (
377+ "Hostname must not be an IP address"
378+ ) ;
379+ } ) ;
380+
381+ it ( "should reject bare IPv6 address" , ( ) => {
382+ expect ( ( ) => validateHostname ( "::1" ) ) . toThrow (
383+ "Hostname must not be an IP address"
384+ ) ;
385+ expect ( ( ) => validateHostname ( "2001:db8::1" ) ) . toThrow (
386+ "Hostname must not be an IP address"
387+ ) ;
388+ expect ( ( ) => validateHostname ( "[::1]" ) ) . toThrow (
389+ "Hostname must not be an IP address"
390+ ) ;
391+ } ) ;
392+
393+ it ( "should reject hostname with port" , ( ) => {
394+ expect ( ( ) => validateHostname ( "example.com:8080" ) ) . toThrow (
395+ "Hostname must not include a port number"
396+ ) ;
397+ } ) ;
398+
399+ it ( "should reject hostname with whitespace" , ( ) => {
400+ expect ( ( ) => validateHostname ( "bad host.com" ) ) . toThrow (
401+ "Hostname must not contain whitespace"
402+ ) ;
403+ } ) ;
404+
405+ it ( "should accept hostnames with underscores" , ( ) => {
406+ expect ( ( ) => validateHostname ( "_dmarc.example.com" ) ) . not . toThrow ( ) ;
407+ expect ( ( ) => validateHostname ( "my_service.internal" ) ) . not . toThrow ( ) ;
408+ } ) ;
409+
410+ it ( "should report all applicable errors at once" , ( ) => {
411+ // "https://example.com/path" has a scheme AND a path
412+ expect ( ( ) => validateHostname ( "https://example.com/path" ) ) . toThrow (
413+ / U R L s c h e m e .* \n .* p a t h / s
414+ ) ;
415+ } ) ;
416+
417+ it ( "should reject invalid hostname via wrangler service create" , async ( ) => {
418+ await expect ( ( ) =>
419+ runWrangler (
420+ "vpc service create test-bad-hostname --type http --hostname https://example.com --tunnel-id 550e8400-e29b-41d4-a716-446655440000"
421+ )
422+ ) . rejects . toThrow ( "Hostname must not include a URL scheme" ) ;
423+ } ) ;
424+
425+ it ( "should reject IP address as hostname via wrangler service create" , async ( ) => {
426+ await expect ( ( ) =>
427+ runWrangler (
428+ "vpc service create test-ip-hostname --type http --hostname 192.168.1.1 --tunnel-id 550e8400-e29b-41d4-a716-446655440000"
429+ )
430+ ) . rejects . toThrow ( "Hostname must not be an IP address" ) ;
431+ } ) ;
432+ } ) ;
433+
434+ describe ( "IP address validation" , ( ) => {
435+ const baseArgs : ServiceArgs = {
436+ name : "test" ,
437+ type : ServiceType . Http ,
438+ tunnelId : "550e8400-e29b-41d4-a716-446655440000" ,
439+ } ;
440+
441+ it ( "should accept valid IPv4 addresses" , ( ) => {
442+ expect ( ( ) =>
443+ validateRequest ( { ...baseArgs , ipv4 : "192.168.1.1" } )
444+ ) . not . toThrow ( ) ;
445+ expect ( ( ) =>
446+ validateRequest ( { ...baseArgs , ipv4 : "10.0.0.1" } )
447+ ) . not . toThrow ( ) ;
448+ } ) ;
449+
450+ it ( "should reject invalid IPv4 addresses" , ( ) => {
451+ expect ( ( ) => validateRequest ( { ...baseArgs , ipv4 : "not-an-ip" } ) ) . toThrow (
452+ "Invalid IPv4 address"
453+ ) ;
454+ expect ( ( ) =>
455+ validateRequest ( { ...baseArgs , ipv4 : "999.999.999.999" } )
456+ ) . toThrow ( "Invalid IPv4 address" ) ;
457+ expect ( ( ) => validateRequest ( { ...baseArgs , ipv4 : "example.com" } ) ) . toThrow (
458+ "Invalid IPv4 address"
459+ ) ;
460+ } ) ;
461+
462+ it ( "should accept valid IPv6 addresses" , ( ) => {
463+ expect ( ( ) => validateRequest ( { ...baseArgs , ipv6 : "::1" } ) ) . not . toThrow ( ) ;
464+ expect ( ( ) =>
465+ validateRequest ( { ...baseArgs , ipv6 : "2001:db8::1" } )
466+ ) . not . toThrow ( ) ;
467+ } ) ;
468+
469+ it ( "should reject invalid IPv6 addresses" , ( ) => {
470+ expect ( ( ) => validateRequest ( { ...baseArgs , ipv6 : "not-an-ip" } ) ) . toThrow (
471+ "Invalid IPv6 address"
472+ ) ;
473+ expect ( ( ) => validateRequest ( { ...baseArgs , ipv6 : "192.168.1.1" } ) ) . toThrow (
474+ "Invalid IPv6 address"
475+ ) ;
476+ } ) ;
477+ } ) ;
478+
328479const mockService : ConnectivityService = {
329480 service_id : "service-uuid" ,
330481 type : ServiceType . Http ,
0 commit comments