@@ -314,4 +314,188 @@ public function test_search_attribute_limit_is_scoped_to_namespace(): void
314314 ], $ this ->apiHeaders ('default ' ))
315315 ->assertCreated ();
316316 }
317+
318+ // ── Per-attribute SA validation at request boundary ─────────────
319+
320+ public function test_workflow_start_rejects_oversized_search_attribute_value (): void
321+ {
322+ config (['server.limits.max_search_attribute_value_bytes ' => 64 ]);
323+
324+ $ this ->configureWorkflowTypes ([
325+ ExternalGreetingWorkflow::class,
326+ ]);
327+
328+ $ this ->postJson ('/api/workflows ' , [
329+ 'workflow_type ' => 'ExternalGreetingWorkflow ' ,
330+ 'search_attributes ' => ['Region ' => str_repeat ('x ' , 200 )],
331+ ], $ this ->apiHeaders ())
332+ ->assertStatus (422 )
333+ ->assertJsonPath (
334+ 'validation_errors.search_attributes.0 ' ,
335+ fn (string $ msg ): bool => str_contains ($ msg , 'Region ' ) && str_contains ($ msg , '64 ' ),
336+ );
337+ }
338+
339+ public function test_workflow_start_rejects_oversized_search_attribute_key (): void
340+ {
341+ config (['server.limits.max_search_attribute_key_length ' => 8 ]);
342+
343+ $ this ->configureWorkflowTypes ([
344+ ExternalGreetingWorkflow::class,
345+ ]);
346+
347+ $ longKey = str_repeat ('K ' , 32 );
348+
349+ $ this ->postJson ('/api/workflows ' , [
350+ 'workflow_type ' => 'ExternalGreetingWorkflow ' ,
351+ 'search_attributes ' => [$ longKey => 'small ' ],
352+ ], $ this ->apiHeaders ())
353+ ->assertStatus (422 )
354+ ->assertJsonPath (
355+ 'validation_errors.search_attributes.0 ' ,
356+ fn (string $ msg ): bool => str_contains ($ msg , $ longKey ) && str_contains ($ msg , '8 ' ),
357+ );
358+ }
359+
360+ public function test_workflow_start_rejects_malformed_search_attribute_key (): void
361+ {
362+ $ this ->configureWorkflowTypes ([
363+ ExternalGreetingWorkflow::class,
364+ ]);
365+
366+ $ this ->postJson ('/api/workflows ' , [
367+ 'workflow_type ' => 'ExternalGreetingWorkflow ' ,
368+ 'search_attributes ' => ['123invalid ' => 'small ' ],
369+ ], $ this ->apiHeaders ())
370+ ->assertStatus (422 )
371+ ->assertJsonPath (
372+ 'validation_errors.search_attributes.0 ' ,
373+ fn (string $ msg ): bool => str_contains ($ msg , '123invalid ' ),
374+ );
375+ }
376+
377+ public function test_workflow_start_rejects_search_attribute_array_with_oversized_element (): void
378+ {
379+ config (['server.limits.max_search_attribute_value_bytes ' => 32 ]);
380+
381+ $ this ->configureWorkflowTypes ([
382+ ExternalGreetingWorkflow::class,
383+ ]);
384+
385+ $ this ->postJson ('/api/workflows ' , [
386+ 'workflow_type ' => 'ExternalGreetingWorkflow ' ,
387+ 'search_attributes ' => ['Tags ' => ['short ' , str_repeat ('y ' , 200 )]],
388+ ], $ this ->apiHeaders ())
389+ ->assertStatus (422 )
390+ ->assertJsonPath (
391+ 'validation_errors.search_attributes.0 ' ,
392+ fn (string $ msg ): bool => str_contains ($ msg , 'Tags ' ) && str_contains ($ msg , '32 ' ),
393+ );
394+ }
395+
396+ public function test_workflow_start_accepts_valid_search_attributes (): void
397+ {
398+ $ this ->configureWorkflowTypes ([
399+ ExternalGreetingWorkflow::class,
400+ ]);
401+
402+ $ this ->postJson ('/api/workflows ' , [
403+ 'workflow_type ' => 'ExternalGreetingWorkflow ' ,
404+ 'search_attributes ' => [
405+ 'Region ' => 'us-east-1 ' ,
406+ 'Priority ' => 5 ,
407+ 'Tags ' => ['alpha ' , 'beta ' ],
408+ ],
409+ ], $ this ->apiHeaders ())
410+ ->assertSuccessful ();
411+ }
412+
413+ // ── Signal / update / query name length validation ─────────────
414+
415+ public function test_workflow_signal_rejects_oversized_name (): void
416+ {
417+ config (['server.limits.max_operation_name_length ' => 16 ]);
418+
419+ $ longName = str_repeat ('A ' , 64 );
420+
421+ $ this ->postJson (
422+ "/api/workflows/wf-any/signal/ {$ longName }" ,
423+ ['input ' => []],
424+ $ this ->apiHeaders (),
425+ )
426+ ->assertStatus (422 )
427+ ->assertJsonPath (
428+ 'validation_errors.signal_name.0 ' ,
429+ fn (string $ msg ): bool => str_contains ($ msg , '16 ' ),
430+ );
431+ }
432+
433+ public function test_workflow_query_rejects_oversized_name (): void
434+ {
435+ config (['server.limits.max_operation_name_length ' => 16 ]);
436+
437+ $ longName = str_repeat ('Q ' , 64 );
438+
439+ $ this ->postJson (
440+ "/api/workflows/wf-any/query/ {$ longName }" ,
441+ ['input ' => []],
442+ $ this ->apiHeaders (),
443+ )
444+ ->assertStatus (422 )
445+ ->assertJsonPath (
446+ 'validation_errors.query_name.0 ' ,
447+ fn (string $ msg ): bool => str_contains ($ msg , '16 ' ),
448+ );
449+ }
450+
451+ public function test_workflow_update_rejects_oversized_name (): void
452+ {
453+ config (['server.limits.max_operation_name_length ' => 16 ]);
454+
455+ $ longName = str_repeat ('U ' , 64 );
456+
457+ $ this ->postJson (
458+ "/api/workflows/wf-any/update/ {$ longName }" ,
459+ ['input ' => []],
460+ $ this ->apiHeaders (),
461+ )
462+ ->assertStatus (422 )
463+ ->assertJsonPath (
464+ 'validation_errors.update_name.0 ' ,
465+ fn (string $ msg ): bool => str_contains ($ msg , '16 ' ),
466+ );
467+ }
468+
469+ public function test_workflow_signal_rejects_control_characters_in_name (): void
470+ {
471+ $ badName = rawurlencode ("my \x01signal " );
472+
473+ $ this ->postJson (
474+ "/api/workflows/wf-any/signal/ {$ badName }" ,
475+ ['input ' => []],
476+ $ this ->apiHeaders (),
477+ )
478+ ->assertStatus (422 )
479+ ->assertJsonPath (
480+ 'validation_errors.signal_name.0 ' ,
481+ fn (string $ msg ): bool => str_contains ($ msg , 'control characters ' ),
482+ );
483+ }
484+
485+ public function test_workflow_signal_name_validation_runs_before_workflow_lookup (): void
486+ {
487+ // Even when the workflow does not exist, the name-validation
488+ // failure must surface as 422 (not 404) so clients learn the
489+ // name itself is the problem.
490+ config (['server.limits.max_operation_name_length ' => 8 ]);
491+
492+ $ longName = str_repeat ('A ' , 16 );
493+
494+ $ this ->postJson (
495+ "/api/workflows/does-not-exist/signal/ {$ longName }" ,
496+ ['input ' => []],
497+ $ this ->apiHeaders (),
498+ )
499+ ->assertStatus (422 );
500+ }
317501}
0 commit comments