13.3 KiB · text History 9c0fc16
defmodule LambentExWeb.ChannelsTest do
  use LambentExWeb.ChannelCase

  alias LambentExWeb.Channels.Payload
  alias LambentExWeb.UserSocket

  @pubsub LambentEx.PubSub

  defp connect_to(topic, params \\ %{}) do
    {:ok, _reply, socket} =
      UserSocket
      |> socket("test-client", %{})
      |> subscribe_and_join(topic, params)

    socket
  end

  describe "payload" do
    test "makes the wire-hostile bits of a publish encodable" do
      device = %{"ip" => {172, 16, 15, 140}, "sox" => 2, "ind" => 7776, "name" => "S5"}

      wired = Payload.device(device)

      assert wired["ip"] == "172.16.15.140"
      assert {:ok, _json} = Jason.encode(wired)
    end

    test "keyword options become objects and atoms become strings" do
      link = %{
        name: "l1",
        chan: :all,
        opts: [name: "l1", source: "src", target: "mac", chan: :all, persist: true]
      }

      wired = Payload.wire(link)

      assert wired["chan"] == "all"
      assert wired["opts"]["persist"] == true
      assert {:ok, _json} = Jason.encode(wired)
    end
  end

  # the clamps publish on their own timer, so the push in flight when a command
  # lands is one from before it. wait for the one that carries the change.
  defp await_clamp(pick, tries \\ 8) do
    assert_push "brightness", payload, 1000

    case pick.(payload) do
      nil when tries > 0 -> await_clamp(pick, tries - 1)
      found -> found
    end
  end

  describe "links made over the channel" do
    test "a link created with a machine's id still finds the machine" do
      {:ok, machine} =
        LambentEx.Machine.start_link(nil,
          name: "channel-source",
          step: LambentEx.Machine.Steps.Solid,
          step_opts: %{h: 84},
          count: 1,
          single: true
        )

      id = LambentEx.Machine.info("channel-source")[:id]
      socket = connect_to("links")

      ref =
        push(socket, "create", %{
          "link" => %{
            "name" => "channel-link",
            "source_id" => id,
            "target_id" => "aa:bb:cc:dd:ee:99",
            "chan" => "all"
          }
        })

      assert_reply ref, :ok

      assert [{pid, _}] = Registry.lookup(:lambent_links, "link-channel-link")
      assert :sys.get_state(pid).source == "channel-source"

      LambentEx.LinkSupervisor.abort_child(pid)
      Process.exit(machine, :kill)
    end
  end

  describe "re-probing" do
    test "a client can ask every device what it runs" do
      socket = connect_to("devices")
      assert_push "devices", %{devices: _snapshot}

      ref = push(socket, "refresh", %{})
      assert_reply ref, :ok
    end
  end

  describe "choosing what gets swept" do
    setup do
      on_exit(fn -> LambentEx.Meta.put_skipped_ifaces([]) end)
      :ok
    end

    test "the devices channel says which interfaces the sweep runs on" do
      connect_to("devices")

      assert_push "interfaces", %{interfaces: interfaces}
      assert is_list(interfaces)
      assert {:ok, _json} = Jason.encode(interfaces)

      # never the ones filtered out by prefix
      refute Enum.any?(interfaces, fn iface -> String.starts_with?(iface.name, "lo") end)
      refute Enum.any?(interfaces, fn iface -> String.starts_with?(iface.name, "docker") end)
    end

    test "turning one off is remembered and pushed back out" do
      socket = connect_to("devices")
      assert_push "interfaces", %{interfaces: _first}

      ref = push(socket, "interface", %{"name" => "test-iface0", "enabled" => false})
      assert_reply ref, :ok

      assert_push "interfaces", %{interfaces: _updated}
      assert "test-iface0" in LambentEx.Meta.get_skipped_ifaces()

      ref = push(socket, "interface", %{"name" => "test-iface0", "enabled" => true})
      assert_reply ref, :ok
      assert_push "interfaces", %{interfaces: _again}
      refute "test-iface0" in LambentEx.Meta.get_skipped_ifaces()
    end
  end

  describe "the machine form over the machines channel" do
    test "describes what it can build and what each type carries" do
      connect_to("machines", %{"previews" => false})

      assert_push "types", %{types: types}
      assert {:ok, _json} = Jason.encode(types)

      chaser = Enum.find(types, fn t -> t.type == "gen_chaser_h" end)
      assert chaser.label == "Chaser (HSV)"

      hue = Enum.find(chaser.fields, fn f -> f.name == "h" end)
      assert hue.kind == "hue"
      assert hue.required

      spacing = Enum.find(chaser.fields, fn f -> f.name == "spacing" end)
      refute spacing.required
      assert spacing.default == 30

      # nothing that has no form behind it
      refute Enum.any?(types, fn t -> t.type == "rcv_gsi" end)

      # the fft receiver picks its broker from the ones the server knows
      fft = Enum.find(types, fn t -> t.type == "rcv_fft" end)
      broker = Enum.find(fft.fields, fn f -> f.name == "broker" end)
      assert broker.kind == "broker"
      assert is_list(broker.options)
    end

    test "previews a configuration without starting it" do
      socket = connect_to("machines", %{"previews" => false})

      ref =
        push(socket, "preview", %{
          "machine" => %{
            "name" => "preview-test",
            "type" => "gen_solid_h",
            "class" => %{"h" => 84, "s" => 255, "v" => 255}
          }
        })

      assert_reply ref, :ok, %{pixels: pixels}
      assert pixels != []
      assert [6, 255, 0] in pixels

      # and nothing was started
      assert Registry.lookup(:lambent_machine, "machine-preview-test") == []
    end

    test "a half filled configuration previews as nothing" do
      socket = connect_to("machines", %{"previews" => false})

      ref =
        push(socket, "preview", %{
          "machine" => %{"name" => "preview-empty", "type" => "gen_solid_h"}
        })

      assert_reply ref, :ok, %{pixels: []}
    end

    test "hands over a catalog entry for editing" do
      socket = connect_to("machines", %{"previews" => false})

      ref = push(socket, "library_edit", %{"cls" => "chase", "entry" => "purple"})
      assert_reply ref, :ok, %{machine: machine}

      assert machine.name == "purple-edited"
      assert machine.type == "gen_chaser_h"
      assert machine.class["h"] == 170
      assert {:ok, _json} = Jason.encode(machine)
    end

    test "refuses an entry with no form behind it" do
      socket = connect_to("machines", %{"previews" => false})

      ref = push(socket, "library_edit", %{"cls" => "solidrgb", "entry" => "ruby"})
      assert_reply ref, :error, %{reason: _why}
    end
  end

  describe "brightness channel" do
    test "pushes the ladders and the clamps on join" do
      connect_to("brightness")

      assert_push "ladder", %{steps: steps, fades: fades}
      assert %{value: 255, name: "FULL"} in steps
      assert %{rate: 120_000, name: "TWOMINS"} in fades
      assert {:ok, _json} = Jason.encode(%{steps: steps, fades: fades})

      assert_push "brightness", %{"global" => global}
      assert global["curr"] != nil
      assert global["rate"] != nil
    end

    test "sets a place clamp and pushes it back" do
      socket = connect_to("brightness")

      ref = push(socket, "set", %{"place" => "channel-test", "value" => 63})
      assert_reply ref, :ok

      clamp = await_clamp(fn payload -> payload["places"]["channel-test"] end)
      assert clamp["tgt"] == 63

      ref = push(socket, "clear", %{"place" => "channel-test"})
      assert_reply ref, :ok
    end

    test "the global clamp is the one with no place" do
      socket = connect_to("brightness")

      ref = push(socket, "set", %{"value" => 127})
      assert_reply ref, :ok

      assert await_clamp(fn payload ->
               if payload["global"]["tgt"] == 127, do: payload["global"]
             end)

      ref = push(socket, "set", %{"value" => 255})
      assert_reply ref, :ok
    end
  end

  describe "scheduling over the services channel" do
    test "schedules a cronos entry" do
      socket = connect_to("services")

      ref =
        push(socket, "create_cronos", %{
          "cronos" => %{
            "name" => "channel-sched",
            "time" => "23:30",
            "action" => "global_bright",
            "arg" => "63"
          }
        })

      assert_reply ref, :ok, %{name: "channel-sched"}

      assert {_name, cfg} =
               LambentEx.Meta.get_cronos() |> Enum.find(fn {k, _v} -> k == "channel-sched" end)

      assert cfg.action == :global_bright
      LambentEx.Meta.del_cronos("channel-sched")
    end

    test "refuses a schedule with nothing to act on" do
      socket = connect_to("services")

      ref =
        push(socket, "create_cronos", %{
          "cronos" => %{"name" => "channel-bad", "time" => "23:30", "action" => "link_on"}
        })

      assert_reply ref, :error, %{errors: errors}
      assert errors[:targets] != nil
    end

    test "offers the same choices the web form does" do
      socket = connect_to("services")

      ref = push(socket, "cronos_options", %{})
      assert_reply ref, :ok

      assert_push "cronos_options", %{actions: actions, args: args}
      assert %{label: "Link on", value: "link_on"} in actions
      assert args["link_on"] == []
      assert {:ok, _json} = Jason.encode(%{actions: actions, args: args})
    end
  end

  describe "devices channel" do
    test "pushes a snapshot on join and forwards publishes" do
      connect_to("devices")
      assert_push "devices", %{devices: _snapshot}

      Phoenix.PubSub.broadcast(
        @pubsub,
        "scan-82667777",
        {:devices_pub, %{"aa:bb" => %{"ip" => {10, 0, 0, 9}, "sox" => 2, "type" => "7777-flex"}}}
      )

      assert_push "devices", %{devices: devices}
      assert devices["aa:bb"]["ip"] == "10.0.0.9"
      assert {:ok, _json} = Jason.encode(devices)
    end

    test "forwards manual probe results" do
      connect_to("devices")

      Phoenix.PubSub.broadcast(@pubsub, "scan-manual", {:manual_result, "10.0.0.9", :not_lambent})

      assert_push "probe_result", %{ip: "10.0.0.9", status: "not_lambent"}
    end

    test "rejects a probe of something that is not an address" do
      socket = connect_to("devices")

      ref = push(socket, "probe", %{"ip" => "nope"})
      assert_reply ref, :error, %{reason: _}
    end

    test "accepts the device controls" do
      socket = connect_to("devices")

      for {cmd, params} <- [
            {"rename", %{"id" => "aa:bb", "name" => "kitchen"}},
            {"replace", %{"id" => "aa:bb", "place" => "cubby"}},
            {"reorder", %{"id" => "aa:bb", "ord" => "grb"}},
            {"poke", %{"id" => "aa:bb", "mode" => "indicator"}}
          ] do
        ref = push(socket, cmd, params)
        assert_reply ref, :ok
      end
    end
  end

  describe "machines channel" do
    test "forwards publishes and the preview firehose" do
      connect_to("machines")

      Phoenix.PubSub.broadcast(
        @pubsub,
        "machines_idx",
        {:machines_pub,
         %{
           id: "SolidxA",
           name: "A",
           step: "Solid",
           speed: 1000,
           opts: [single: true],
           bgt: 255,
           persist: false
         }}
      )

      assert_push "machine", machine
      assert machine["opts"]["single"] == true
      assert {:ok, _json} = Jason.encode(machine)

      Phoenix.PubSub.broadcast(
        @pubsub,
        "machine:",
        {:firehose, {"A", [[255, 0, 0], [0, 255, 0]]}}
      )

      assert_push "preview", %{name: "A", pixels: [[255, 0, 0], [0, 255, 0]]}
    end

    test "pushes the library on join, so a client can just listen" do
      connect_to("machines")

      assert_push "library", %{library: library}
      assert {:ok, _json} = Jason.encode(library)
      assert map_size(library) > 0
    end

    test "serves the library again on request" do
      socket = connect_to("machines")
      assert_push "library", %{library: _}

      ref = push(socket, "library", %{})
      assert_reply ref, :ok, %{library: library}
      assert_push "library", %{library: ^library}
    end

    test "refuses a library entry that does not exist" do
      socket = connect_to("machines")

      ref = push(socket, "library_add", %{"cls" => "nope", "entry" => "nope"})
      assert_reply ref, :error, %{reason: _}
    end

    test "skips the firehose when asked to" do
      connect_to("machines", %{"previews" => false})

      Phoenix.PubSub.broadcast(@pubsub, "machine:", {:firehose, {"A", [[1, 2, 3]]}})

      refute_push "preview", %{}
    end
  end

  describe "links channel" do
    test "forwards publishes with the channel the link drives" do
      connect_to("links")

      Phoenix.PubSub.broadcast(
        @pubsub,
        "links_idx",
        {:links_pub,
         %{
           name: "l1",
           source: "SolidxA",
           target: "aa:bb",
           chan: 1,
           enabled: true,
           persist: false,
           opts: []
         }}
      )

      assert_push "link", link
      assert link["chan"] == 1
      assert {:ok, _json} = Jason.encode(link)
    end

    test "reports changeset errors on a bad create" do
      socket = connect_to("links")

      ref = push(socket, "create", %{"link" => %{"name" => "only-a-name"}})
      assert_reply ref, :error, %{errors: errors}
      assert errors[:target_id] != nil
    end
  end

  describe "services channel" do
    test "pushes each configured kind on join" do
      connect_to("services")

      assert_push "services", %{kind: "http", services: _}
      assert_push "services", %{kind: "mqtt", services: _}
      assert_push "services", %{kind: "cronos", services: _}
    end
  end
end