@@ -421,6 +421,199 @@ def test_render_basic_plan(self, sample_terraform_plan):
421421 assert "DeployDiff" in output
422422 assert "Change Summary" in output
423423
424+ def test_render_empty_plan (self ):
425+ """Render an empty plan shows no changes."""
426+ from io import StringIO
427+ from rich .console import Console
428+ plan = DeployPlan (source = ChangeSource .TERRAFORM , changes = [])
429+ buf = StringIO ()
430+ console = Console (file = buf , force_terminal = True )
431+ render_plan (plan , console )
432+ output = buf .getvalue ()
433+ assert "DeployDiff" in output
434+ assert "0 resource(s)" not in output
435+
436+ def test_render_verbose_terraform (self , sample_terraform_plan ):
437+ """Verbose mode shows before/after details for each change."""
438+ from io import StringIO
439+ from rich .console import Console
440+ plan = parse_terraform_plan (sample_terraform_plan )
441+ buf = StringIO ()
442+ console = Console (file = buf , force_terminal = True )
443+ render_plan (plan , console , verbose = True )
444+ output = buf .getvalue ()
445+ assert "instance_type" in output
446+ assert "t3.micro" in output
447+
448+ def test_render_verbose_with_sensitive (self ):
449+ """Verbose mode masks sensitive values."""
450+ from io import StringIO
451+ from rich .console import Console
452+ change = ResourceChange (
453+ address = "aws_db_instance.db" ,
454+ action = ChangeAction .UPDATE ,
455+ resource_type = "aws_db_instance" ,
456+ resource_name = "db" ,
457+ source = ChangeSource .TERRAFORM ,
458+ before = {"password" : "secret123" , "port" : 5432 },
459+ after = {"password" : "newsecret" , "port" : 5432 },
460+ before_sensitive = {"password" },
461+ after_sensitive = {"password" },
462+ )
463+ plan = DeployPlan (source = ChangeSource .TERRAFORM , changes = [change ])
464+ buf = StringIO ()
465+ console = Console (file = buf , force_terminal = True )
466+ render_plan (plan , console , verbose = True )
467+ output = buf .getvalue ()
468+ # Check for "sensitive value" text (may be split by ANSI codes around parentheses)
469+ assert "sensitive value" in output
470+ assert "secret123" not in output
471+ assert "5432" in output
472+
473+ def test_render_destructive_change_warning (self , sample_terraform_plan ):
474+ """Destructive changes trigger a warning message."""
475+ from io import StringIO
476+ from rich .console import Console
477+ plan = parse_terraform_plan (sample_terraform_plan )
478+ buf = StringIO ()
479+ console = Console (file = buf , force_terminal = True )
480+ render_plan (plan , console )
481+ output = buf .getvalue ()
482+ # "destructive" appears contiguously even with ANSI codes
483+ assert "destructive" in output .lower ()
484+
485+ def test_render_plan_without_destructive_changes (self ):
486+ """Plan with only creates/updates should not show destructive warning."""
487+ from io import StringIO
488+ from rich .console import Console
489+ changes = [
490+ ResourceChange (
491+ address = "aws_instance.web" ,
492+ action = ChangeAction .CREATE ,
493+ resource_type = "aws_instance" ,
494+ resource_name = "web" ,
495+ source = ChangeSource .TERRAFORM ,
496+ ),
497+ ResourceChange (
498+ address = "aws_db_instance.db" ,
499+ action = ChangeAction .UPDATE ,
500+ resource_type = "aws_db_instance" ,
501+ resource_name = "db" ,
502+ source = ChangeSource .TERRAFORM ,
503+ ),
504+ ]
505+ plan = DeployPlan (source = ChangeSource .TERRAFORM , changes = changes )
506+ buf = StringIO ()
507+ console = Console (file = buf , force_terminal = True )
508+ render_plan (plan , console )
509+ output = buf .getvalue ()
510+ assert "destructive" not in output .lower ()
511+
512+ def test_render_cfn_plan (self , sample_cfn_changeset ):
513+ """Render a CloudFormation plan."""
514+ from io import StringIO
515+ from rich .console import Console
516+ plan = parse_cloudformation_changeset (sample_cfn_changeset )
517+ buf = StringIO ()
518+ console = Console (file = buf , force_terminal = True )
519+ render_plan (plan , console )
520+ output = buf .getvalue ()
521+ assert "Cloudformation" in output or "CloudFormation" in output
522+ assert "Change Summary" in output
523+
524+ def test_render_pulumi_plan (self , sample_pulumi_preview ):
525+ """Render a Pulumi plan."""
526+ from io import StringIO
527+ from rich .console import Console
528+ plan = parse_pulumi_preview (sample_pulumi_preview )
529+ buf = StringIO ()
530+ console = Console (file = buf , force_terminal = True )
531+ render_plan (plan , console )
532+ output = buf .getvalue ()
533+ assert "Pulumi" in output
534+
535+ def test_render_replacement (self ):
536+ """Render a plan with a replacement change."""
537+ from io import StringIO
538+ from rich .console import Console
539+ change = ResourceChange (
540+ address = "module.vpc.aws_nat_gateway.main" ,
541+ action = ChangeAction .REPLACE ,
542+ resource_type = "aws_nat_gateway" ,
543+ resource_name = "main" ,
544+ source = ChangeSource .TERRAFORM ,
545+ before = {"connectivity_type" : "public" },
546+ after = {"connectivity_type" : "private" },
547+ module_path = "module.vpc" ,
548+ )
549+ plan = DeployPlan (source = ChangeSource .TERRAFORM , changes = [change ])
550+ buf = StringIO ()
551+ console = Console (file = buf , force_terminal = True )
552+ render_plan (plan , console )
553+ output = buf .getvalue ()
554+ assert "⇄" in output or "will be replaced" in output .lower ()
555+
556+ def test_render_change_details_missing_data (self ):
557+ """Render change details with no before/after should not error."""
558+ from io import StringIO
559+ from rich .console import Console
560+ from deploydiff .diff_renderer import _render_change_details
561+ change = ResourceChange (
562+ address = "aws_instance.web" ,
563+ action = ChangeAction .CREATE ,
564+ resource_type = "aws_instance" ,
565+ resource_name = "web" ,
566+ source = ChangeSource .TERRAFORM ,
567+ before = None ,
568+ after = None ,
569+ )
570+ buf = StringIO ()
571+ console = Console (file = buf , force_terminal = True )
572+ # Should not raise
573+ _render_change_details (change , console )
574+ output = buf .getvalue ()
575+ assert output == ""
576+
577+ def test_group_by_action (self ):
578+ """Grouping changes by action produces correct buckets."""
579+ from deploydiff .diff_renderer import _group_by_action
580+ changes = [
581+ ResourceChange ("a" , ChangeAction .CREATE , "t" , "n" , ChangeSource .TERRAFORM ),
582+ ResourceChange ("b" , ChangeAction .CREATE , "t" , "n" , ChangeSource .TERRAFORM ),
583+ ResourceChange ("c" , ChangeAction .UPDATE , "t" , "n" , ChangeSource .TERRAFORM ),
584+ ResourceChange ("d" , ChangeAction .DELETE , "t" , "n" , ChangeSource .TERRAFORM ),
585+ ]
586+ plan = DeployPlan (source = ChangeSource .TERRAFORM , changes = changes )
587+ groups = _group_by_action (plan )
588+ assert len (groups [ChangeAction .CREATE ]) == 2
589+ assert len (groups [ChangeAction .UPDATE ]) == 1
590+ assert len (groups [ChangeAction .DELETE ]) == 1
591+ assert ChangeAction .CREATE_BEFORE_DELETE not in groups
592+
593+ def test_render_console_none (self ):
594+ """Renderer creates its own Console if none is provided."""
595+ plan = DeployPlan (source = ChangeSource .TERRAFORM , changes = [])
596+ # Should not raise when console is None
597+ render_plan (plan )
598+
599+ def test_render_create_before_delete_action_label (self ):
600+ """Create-before-delete action has the right label."""
601+ from deploydiff .diff_renderer import ACTION_LABELS
602+ label = ACTION_LABELS [ChangeAction .CREATE_BEFORE_DELETE ]
603+ assert "create-first" in label
604+
605+ def test_render_no_op_label (self ):
606+ """No-op action has the right label."""
607+ from deploydiff .diff_renderer import ACTION_LABELS
608+ label = ACTION_LABELS [ChangeAction .NO_OP ]
609+ assert label == "no changes"
610+
611+ def test_render_import_action_label (self ):
612+ """Import action has the right label."""
613+ from deploydiff .diff_renderer import ACTION_LABELS
614+ label = ACTION_LABELS [ChangeAction .IMPORT ]
615+ assert "imported" in label
616+
424617
425618# ── CLI Integration Tests ─────────────────────────────────────────────────
426619
0 commit comments