1
0

render_test.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. package commands
  2. import (
  3. "bytes"
  4. "io"
  5. "testing"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/require"
  8. )
  9. // TestRenderPrintsAnOutcomeWithoutText: a command that acts declares no Text. Its
  10. // outcome renders as the reason, the same for every command, and as {ok, reason} in
  11. // JSON, which is the shape these commands have always emitted.
  12. func TestRenderPrintsAnOutcomeWithoutText(t *testing.T) {
  13. c := &Command{Name: "act"}
  14. out := &bytes.Buffer{}
  15. require.NoError(t, Render(c, Did("Closed"), FormatText, out))
  16. assert.Equal(t, "Closed\n", out.String())
  17. out.Reset()
  18. require.NoError(t, Render(c, Failed("No vpn address was provided"), FormatText, out))
  19. assert.Equal(t, "No vpn address was provided\n", out.String())
  20. out.Reset()
  21. require.NoError(t, Render(c, Failed("nope"), FormatJSON, out))
  22. assert.JSONEq(t, `{"ok":false,"reason":"nope"}`, out.String())
  23. }
  24. // TestRenderHandsTextOnlyTheValue pins the guarantee Result exists for: a command's
  25. // renderer sees the type it returns on success and never the envelope or an outcome, so
  26. // a report command that refuses its arguments needs nothing in its Text to cope.
  27. func TestRenderHandsTextOnlyTheValue(t *testing.T) {
  28. var got any
  29. c := &Command{
  30. Name: "report",
  31. Text: func(v any, w io.Writer) error {
  32. got = v
  33. _, err := io.WriteString(w, "rendered\n")
  34. return err
  35. },
  36. }
  37. out := &bytes.Buffer{}
  38. require.NoError(t, Render(c, Value("data"), FormatText, out))
  39. assert.Equal(t, "data", got)
  40. assert.Equal(t, "rendered\n", out.String())
  41. got, out = nil, &bytes.Buffer{}
  42. require.NoError(t, Render(c, Failed("refused"), FormatText, out))
  43. assert.Nil(t, got, "Text must not be called for an outcome")
  44. assert.Equal(t, "refused\n", out.String())
  45. out.Reset()
  46. require.NoError(t, Render(c, Value("data"), FormatJSON, out))
  47. assert.JSONEq(t, `"data"`, out.String(), "JSON carries the value, never the envelope")
  48. }
  49. // TestRenderRefusesTextForAValueWithoutText: leaving Text nil is a statement that no
  50. // human form of the value exists yet, and asking for one is an error that says so.
  51. func TestRenderRefusesTextForAValueWithoutText(t *testing.T) {
  52. out := &bytes.Buffer{}
  53. err := Render(&Command{Name: "print-tunnel"}, Value(map[string]int{"index": 7}), FormatText, out)
  54. require.ErrorIs(t, err, ErrNoTextRendering)
  55. require.ErrorContains(t, err, "print-tunnel has no human readable output yet")
  56. assert.Empty(t, out.String())
  57. }
  58. // TestTypedNamesTheTypeOnce: Typed replaces the assertion at the top of every renderer.
  59. // The right type reaches the renderer; the wrong one is reported, naming both.
  60. func TestTypedNamesTheTypeOnce(t *testing.T) {
  61. type info struct{ Name string }
  62. render := Typed(func(i info, w io.Writer) error {
  63. _, err := io.WriteString(w, i.Name+"\n")
  64. return err
  65. })
  66. out := &bytes.Buffer{}
  67. require.NoError(t, render(info{Name: "tun0"}, out))
  68. assert.Equal(t, "tun0\n", out.String())
  69. err := render("not an info", out)
  70. require.Error(t, err)
  71. assert.ErrorContains(t, err, "expected commands.info, got string")
  72. }
  73. // TestResultShapes pins what each constructor means to a surface: Empty is the one that
  74. // renders nothing, Payload is what a program receives, and OK is set wherever a reason
  75. // or value says the command worked.
  76. func TestResultShapes(t *testing.T) {
  77. assert.True(t, Nothing().Empty())
  78. assert.True(t, Result{}.Empty(), "the zero Result says nothing rather than failing silently")
  79. assert.False(t, Did("done").Empty())
  80. assert.False(t, Failed("no").Empty())
  81. assert.False(t, Value("x").Empty())
  82. assert.True(t, Value("x").OK)
  83. assert.True(t, Did("done").OK)
  84. assert.False(t, Failed("no").OK)
  85. assert.Equal(t, "x", Value("x").Payload())
  86. assert.Equal(t, Outcome{OK: true, Reason: "done"}, Did("done").Payload())
  87. assert.Equal(t, Outcome{Reason: "no"}, Failed("no").Payload())
  88. }
  89. // TestNotFoundIsARefusalLikeAnyOther: NotFound renders and marshals exactly as Failed
  90. // does, so no surface's output changes by a command adopting it. The mark is on the
  91. // Result alone, for a surface whose protocol has a word for it.
  92. func TestNotFoundIsARefusalLikeAnyOther(t *testing.T) {
  93. r := NotFound("No active tunnel for vpn addr: %s", "10.0.0.5")
  94. assert.False(t, r.OK)
  95. assert.True(t, r.NotFound)
  96. assert.False(t, Failed("nope").NotFound)
  97. out := &bytes.Buffer{}
  98. require.NoError(t, Render(&Command{Name: "look"}, r, FormatText, out))
  99. assert.Equal(t, "No active tunnel for vpn addr: 10.0.0.5\n", out.String())
  100. out.Reset()
  101. require.NoError(t, Render(&Command{Name: "look"}, r, FormatJSON, out))
  102. assert.JSONEq(t, `{"ok":false,"reason":"No active tunnel for vpn addr: 10.0.0.5"}`, out.String(), "the mark never reaches the wire")
  103. }