add parallel build
This commit is contained in:
parent
00f704cd83
commit
b8b3c5dce0
5 changed files with 1214 additions and 22 deletions
23
Dockerfile
23
Dockerfile
|
|
@ -21,19 +21,20 @@ ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION
|
|||
ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}"
|
||||
|
||||
FROM ${BUILDER_IMAGE} AS builder
|
||||
ENV MIX_OS_DEPS_COMPILE_PARTITION_COUNT=6
|
||||
|
||||
# install build dependencies
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get upgrade -y \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get upgrade -y \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# prepare build dir
|
||||
WORKDIR /app
|
||||
|
||||
# install hex + rebar
|
||||
RUN mix local.hex --force \
|
||||
&& mix local.rebar --force
|
||||
&& mix local.rebar --force
|
||||
|
||||
# set build ENV
|
||||
ENV MIX_ENV="prod"
|
||||
|
|
@ -80,13 +81,13 @@ RUN mix release
|
|||
FROM ${RUNNER_IMAGE} AS final
|
||||
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get upgrade -y \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 locales ca-certificates snmp snmp-mibs-downloader \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get upgrade -y \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 locales ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the locale
|
||||
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \
|
||||
&& locale-gen
|
||||
&& locale-gen
|
||||
|
||||
ENV LANG=en_US.UTF-8
|
||||
ENV LANGUAGE=en_US:en
|
||||
|
|
@ -101,10 +102,6 @@ ENV MIX_ENV="prod"
|
|||
# Only copy the final release from the build stage
|
||||
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/towerops ./
|
||||
|
||||
# Copy MIB files for SNMP MIB name resolution
|
||||
# LibreNMS MIBs will be mounted or copied separately
|
||||
RUN mkdir -p /app/mibs
|
||||
|
||||
USER nobody
|
||||
|
||||
# If using an environment that doesn't automatically reap zombie processes, it is
|
||||
|
|
@ -114,6 +111,6 @@ USER nobody
|
|||
|
||||
# Health check for container orchestration
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD ["/app/bin/towerops", "rpc", "1 + 1"]
|
||||
CMD ["/app/bin/towerops", "rpc", "1 + 1"]
|
||||
|
||||
CMD ["/app/bin/server"]
|
||||
|
|
|
|||
|
|
@ -21,19 +21,20 @@ ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION
|
|||
ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}"
|
||||
|
||||
FROM ${BUILDER_IMAGE} AS builder
|
||||
ENV MIX_OS_DEPS_COMPILE_PARTITION_COUNT=6
|
||||
|
||||
# install build dependencies
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get upgrade -y \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get upgrade -y \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# prepare build dir
|
||||
WORKDIR /app
|
||||
|
||||
# install hex + rebar
|
||||
RUN mix local.hex --force \
|
||||
&& mix local.rebar --force
|
||||
&& mix local.rebar --force
|
||||
|
||||
# set build ENV
|
||||
ENV MIX_ENV="prod"
|
||||
|
|
@ -80,13 +81,13 @@ RUN mix release
|
|||
FROM ${RUNNER_IMAGE} AS final
|
||||
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get upgrade -y \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 locales ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get upgrade -y \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 locales ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the locale
|
||||
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \
|
||||
&& locale-gen
|
||||
&& locale-gen
|
||||
|
||||
ENV LANG=en_US.UTF-8
|
||||
ENV LANGUAGE=en_US:en
|
||||
|
|
@ -110,6 +111,6 @@ USER nobody
|
|||
|
||||
# Health check for container orchestration
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD ["/app/bin/towerops", "rpc", "1 + 1"]
|
||||
CMD ["/app/bin/towerops", "rpc", "1 + 1"]
|
||||
|
||||
CMD ["/app/bin/server"]
|
||||
|
|
|
|||
154
test/towerops/api_tokens/api_token_test.exs
Normal file
154
test/towerops/api_tokens/api_token_test.exs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
defmodule Towerops.ApiTokens.ApiTokenTest do
|
||||
use Towerops.DataCase
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.ApiTokens.ApiToken
|
||||
|
||||
describe "changeset/2" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
%{user: user, organization: organization}
|
||||
end
|
||||
|
||||
test "valid changeset with all required fields", %{
|
||||
user: user,
|
||||
organization: organization
|
||||
} do
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Test Token",
|
||||
token_hash: "abc123"
|
||||
}
|
||||
|
||||
changeset = ApiToken.changeset(%ApiToken{}, attrs)
|
||||
assert changeset.valid?
|
||||
end
|
||||
|
||||
test "valid changeset without user_id", %{organization: organization} do
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
name: "Test Token",
|
||||
token_hash: "abc123"
|
||||
}
|
||||
|
||||
changeset = ApiToken.changeset(%ApiToken{}, attrs)
|
||||
assert changeset.valid?
|
||||
end
|
||||
|
||||
test "valid changeset with expires_at", %{organization: organization} do
|
||||
expires_at = DateTime.add(DateTime.utc_now(), 30, :day)
|
||||
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
name: "Test Token",
|
||||
token_hash: "abc123",
|
||||
expires_at: expires_at
|
||||
}
|
||||
|
||||
changeset = ApiToken.changeset(%ApiToken{}, attrs)
|
||||
assert changeset.valid?
|
||||
end
|
||||
|
||||
test "valid changeset with last_used_at", %{organization: organization} do
|
||||
last_used_at = DateTime.utc_now()
|
||||
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
name: "Test Token",
|
||||
token_hash: "abc123",
|
||||
last_used_at: last_used_at
|
||||
}
|
||||
|
||||
changeset = ApiToken.changeset(%ApiToken{}, attrs)
|
||||
assert changeset.valid?
|
||||
end
|
||||
|
||||
test "invalid changeset without organization_id", %{user: user} do
|
||||
attrs = %{
|
||||
user_id: user.id,
|
||||
name: "Test Token",
|
||||
token_hash: "abc123"
|
||||
}
|
||||
|
||||
changeset = ApiToken.changeset(%ApiToken{}, attrs)
|
||||
refute changeset.valid?
|
||||
assert "can't be blank" in errors_on(changeset).organization_id
|
||||
end
|
||||
|
||||
test "invalid changeset without name", %{organization: organization} do
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
token_hash: "abc123"
|
||||
}
|
||||
|
||||
changeset = ApiToken.changeset(%ApiToken{}, attrs)
|
||||
refute changeset.valid?
|
||||
assert "can't be blank" in errors_on(changeset).name
|
||||
end
|
||||
|
||||
test "invalid changeset without token_hash", %{organization: organization} do
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
name: "Test Token"
|
||||
}
|
||||
|
||||
changeset = ApiToken.changeset(%ApiToken{}, attrs)
|
||||
refute changeset.valid?
|
||||
assert "can't be blank" in errors_on(changeset).token_hash
|
||||
end
|
||||
|
||||
test "invalid changeset with blank name", %{organization: organization} do
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
name: "",
|
||||
token_hash: "abc123"
|
||||
}
|
||||
|
||||
changeset = ApiToken.changeset(%ApiToken{}, attrs)
|
||||
refute changeset.valid?
|
||||
assert "can't be blank" in errors_on(changeset).name
|
||||
end
|
||||
|
||||
test "invalid changeset with name too short", %{organization: organization} do
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
name: "",
|
||||
token_hash: "abc123"
|
||||
}
|
||||
|
||||
changeset = ApiToken.changeset(%ApiToken{}, attrs)
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "invalid changeset with name too long", %{organization: organization} do
|
||||
long_name = String.duplicate("a", 256)
|
||||
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
name: long_name,
|
||||
token_hash: "abc123"
|
||||
}
|
||||
|
||||
changeset = ApiToken.changeset(%ApiToken{}, attrs)
|
||||
refute changeset.valid?
|
||||
assert "should be at most 255 character(s)" in errors_on(changeset).name
|
||||
end
|
||||
|
||||
test "changeset with maximum valid name length", %{organization: organization} do
|
||||
max_name = String.duplicate("a", 255)
|
||||
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
name: max_name,
|
||||
token_hash: "abc123"
|
||||
}
|
||||
|
||||
changeset = ApiToken.changeset(%ApiToken{}, attrs)
|
||||
assert changeset.valid?
|
||||
end
|
||||
end
|
||||
end
|
||||
471
test/towerops/api_tokens_test.exs
Normal file
471
test/towerops/api_tokens_test.exs
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
defmodule Towerops.ApiTokensTest do
|
||||
use Towerops.DataCase
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.ApiTokens
|
||||
alias Towerops.ApiTokens.ApiToken
|
||||
|
||||
describe "create_api_token/1" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
%{user: user, organization: organization}
|
||||
end
|
||||
|
||||
test "creates an API token with valid attributes", %{
|
||||
user: user,
|
||||
organization: organization
|
||||
} do
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Test Token"
|
||||
}
|
||||
|
||||
assert {:ok, {api_token, raw_token}} = ApiTokens.create_api_token(attrs)
|
||||
assert api_token.name == "Test Token"
|
||||
assert api_token.organization_id == organization.id
|
||||
assert api_token.user_id == user.id
|
||||
assert is_binary(api_token.token_hash)
|
||||
assert String.starts_with?(raw_token, "towerops_")
|
||||
assert is_nil(api_token.last_used_at)
|
||||
assert is_nil(api_token.expires_at)
|
||||
end
|
||||
|
||||
test "creates an API token without user_id", %{organization: organization} do
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
name: "Test Token"
|
||||
}
|
||||
|
||||
assert {:ok, {api_token, raw_token}} = ApiTokens.create_api_token(attrs)
|
||||
assert api_token.name == "Test Token"
|
||||
assert api_token.organization_id == organization.id
|
||||
assert is_nil(api_token.user_id)
|
||||
assert is_binary(api_token.token_hash)
|
||||
assert String.starts_with?(raw_token, "towerops_")
|
||||
end
|
||||
|
||||
test "creates an API token with expiration", %{
|
||||
user: user,
|
||||
organization: organization
|
||||
} do
|
||||
expires_at = DateTime.add(DateTime.utc_now(), 30, :day)
|
||||
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Expiring Token",
|
||||
expires_at: expires_at
|
||||
}
|
||||
|
||||
assert {:ok, {api_token, _raw_token}} = ApiTokens.create_api_token(attrs)
|
||||
assert api_token.expires_at == DateTime.truncate(expires_at, :second)
|
||||
end
|
||||
|
||||
test "returns error with invalid attributes" do
|
||||
assert {:error, %Ecto.Changeset{}} = ApiTokens.create_api_token(%{})
|
||||
end
|
||||
|
||||
test "returns error with blank name", %{organization: organization} do
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
name: ""
|
||||
}
|
||||
|
||||
assert {:error, changeset} = ApiTokens.create_api_token(attrs)
|
||||
assert "can't be blank" in errors_on(changeset).name
|
||||
end
|
||||
|
||||
test "returns error without organization_id" do
|
||||
attrs = %{
|
||||
name: "Test Token"
|
||||
}
|
||||
|
||||
assert {:error, changeset} = ApiTokens.create_api_token(attrs)
|
||||
assert "can't be blank" in errors_on(changeset).organization_id
|
||||
end
|
||||
|
||||
test "generates unique tokens", %{user: user, organization: organization} do
|
||||
attrs = %{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Test Token"
|
||||
}
|
||||
|
||||
{:ok, {_token1, raw_token1}} = ApiTokens.create_api_token(attrs)
|
||||
{:ok, {_token2, raw_token2}} = ApiTokens.create_api_token(attrs)
|
||||
|
||||
assert raw_token1 != raw_token2
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_organization_api_tokens/1" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
%{user: user, organization: organization}
|
||||
end
|
||||
|
||||
test "returns all tokens for an organization", %{
|
||||
user: user,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, {token1, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Token 1"
|
||||
})
|
||||
|
||||
{:ok, {token2, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Token 2"
|
||||
})
|
||||
|
||||
tokens = ApiTokens.list_organization_api_tokens(organization.id)
|
||||
|
||||
assert length(tokens) == 2
|
||||
assert Enum.any?(tokens, fn t -> t.id == token1.id end)
|
||||
assert Enum.any?(tokens, fn t -> t.id == token2.id end)
|
||||
end
|
||||
|
||||
test "returns tokens ordered by creation date", %{
|
||||
user: user,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, {token1, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Token 1"
|
||||
})
|
||||
|
||||
{:ok, {token2, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Token 2"
|
||||
})
|
||||
|
||||
tokens = ApiTokens.list_organization_api_tokens(organization.id)
|
||||
|
||||
assert length(tokens) == 2
|
||||
# Verify tokens are ordered by inserted_at (descending)
|
||||
[first, second] = tokens
|
||||
|
||||
assert DateTime.compare(first.inserted_at, second.inserted_at) in [:gt, :eq]
|
||||
# Verify both tokens are present
|
||||
token_ids = Enum.map(tokens, & &1.id)
|
||||
assert token1.id in token_ids
|
||||
assert token2.id in token_ids
|
||||
end
|
||||
|
||||
test "returns empty list for organization with no tokens", %{organization: organization} do
|
||||
assert [] = ApiTokens.list_organization_api_tokens(organization.id)
|
||||
end
|
||||
|
||||
test "does not return tokens from other organizations", %{user: user, organization: org1} do
|
||||
org2 = organization_fixture(user.id)
|
||||
|
||||
{:ok, {_token1, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: org1.id,
|
||||
user_id: user.id,
|
||||
name: "Org 1 Token"
|
||||
})
|
||||
|
||||
{:ok, {_token2, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: org2.id,
|
||||
user_id: user.id,
|
||||
name: "Org 2 Token"
|
||||
})
|
||||
|
||||
tokens = ApiTokens.list_organization_api_tokens(org1.id)
|
||||
|
||||
assert length(tokens) == 1
|
||||
assert hd(tokens).name == "Org 1 Token"
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_user_api_tokens/1" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
%{user: user, organization: organization}
|
||||
end
|
||||
|
||||
test "returns all tokens created by a user", %{user: user, organization: organization} do
|
||||
{:ok, {token1, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Token 1"
|
||||
})
|
||||
|
||||
{:ok, {token2, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Token 2"
|
||||
})
|
||||
|
||||
tokens = ApiTokens.list_user_api_tokens(user.id)
|
||||
|
||||
assert length(tokens) == 2
|
||||
assert Enum.any?(tokens, fn t -> t.id == token1.id end)
|
||||
assert Enum.any?(tokens, fn t -> t.id == token2.id end)
|
||||
end
|
||||
|
||||
test "preloads organization", %{user: user, organization: organization} do
|
||||
{:ok, {_token, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Token 1"
|
||||
})
|
||||
|
||||
tokens = ApiTokens.list_user_api_tokens(user.id)
|
||||
|
||||
assert [token] = tokens
|
||||
assert %Ecto.Association.NotLoaded{} != token.organization
|
||||
assert token.organization.id == organization.id
|
||||
end
|
||||
|
||||
test "returns tokens ordered by creation date", %{
|
||||
user: user,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, {token1, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Token 1"
|
||||
})
|
||||
|
||||
{:ok, {token2, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Token 2"
|
||||
})
|
||||
|
||||
tokens = ApiTokens.list_user_api_tokens(user.id)
|
||||
|
||||
assert length(tokens) == 2
|
||||
# Verify tokens are ordered by inserted_at (descending)
|
||||
[first, second] = tokens
|
||||
|
||||
assert DateTime.compare(first.inserted_at, second.inserted_at) in [:gt, :eq]
|
||||
# Verify both tokens are present
|
||||
token_ids = Enum.map(tokens, & &1.id)
|
||||
assert token1.id in token_ids
|
||||
assert token2.id in token_ids
|
||||
end
|
||||
|
||||
test "returns empty list for user with no tokens", %{user: user} do
|
||||
assert [] = ApiTokens.list_user_api_tokens(user.id)
|
||||
end
|
||||
|
||||
test "does not return tokens created by other users", %{organization: organization} do
|
||||
user1 = user_fixture()
|
||||
user2 = user_fixture()
|
||||
|
||||
{:ok, {_token1, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user1.id,
|
||||
name: "User 1 Token"
|
||||
})
|
||||
|
||||
{:ok, {_token2, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user2.id,
|
||||
name: "User 2 Token"
|
||||
})
|
||||
|
||||
tokens = ApiTokens.list_user_api_tokens(user1.id)
|
||||
|
||||
assert length(tokens) == 1
|
||||
assert hd(tokens).name == "User 1 Token"
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_api_token!/1" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
|
||||
{:ok, {token, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Test Token"
|
||||
})
|
||||
|
||||
%{token: token}
|
||||
end
|
||||
|
||||
test "returns the token with the given id", %{token: token} do
|
||||
fetched_token = ApiTokens.get_api_token!(token.id)
|
||||
assert fetched_token.id == token.id
|
||||
assert fetched_token.name == token.name
|
||||
end
|
||||
|
||||
test "raises if the token does not exist" do
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
ApiTokens.get_api_token!(Ecto.UUID.generate())
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "verify_token/1" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
|
||||
{:ok, {token, raw_token}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Test Token"
|
||||
})
|
||||
|
||||
%{token: token, raw_token: raw_token, organization: organization, user: user}
|
||||
end
|
||||
|
||||
test "returns organization_id and user for valid token", %{
|
||||
raw_token: raw_token,
|
||||
organization: organization,
|
||||
user: user
|
||||
} do
|
||||
assert {:ok, org_id, returned_user} = ApiTokens.verify_token(raw_token)
|
||||
assert org_id == organization.id
|
||||
assert returned_user.id == user.id
|
||||
end
|
||||
|
||||
test "updates last_used_at timestamp", %{token: token, raw_token: raw_token} do
|
||||
assert is_nil(token.last_used_at)
|
||||
|
||||
{:ok, _org_id, _user} = ApiTokens.verify_token(raw_token)
|
||||
|
||||
# Give the async Task time to complete
|
||||
Process.sleep(50)
|
||||
|
||||
updated_token = ApiTokens.get_api_token!(token.id)
|
||||
assert updated_token.last_used_at
|
||||
assert DateTime.diff(updated_token.last_used_at, DateTime.utc_now(), :second) <= 1
|
||||
end
|
||||
|
||||
test "returns error for invalid token" do
|
||||
assert {:error, :invalid_token} = ApiTokens.verify_token("invalid_token")
|
||||
end
|
||||
|
||||
test "returns error for expired token", %{
|
||||
user: user,
|
||||
organization: organization
|
||||
} do
|
||||
expires_at = DateTime.add(DateTime.utc_now(), -1, :day)
|
||||
|
||||
{:ok, {_token, raw_token}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Expired Token",
|
||||
expires_at: expires_at
|
||||
})
|
||||
|
||||
assert {:error, :invalid_token} = ApiTokens.verify_token(raw_token)
|
||||
end
|
||||
|
||||
test "works for token without expiration", %{
|
||||
raw_token: raw_token,
|
||||
organization: organization
|
||||
} do
|
||||
assert {:ok, org_id, _user} = ApiTokens.verify_token(raw_token)
|
||||
assert org_id == organization.id
|
||||
end
|
||||
|
||||
test "works for token without user_id", %{organization: organization} do
|
||||
{:ok, {_token, raw_token}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
name: "No User Token"
|
||||
})
|
||||
|
||||
assert {:ok, org_id, nil} = ApiTokens.verify_token(raw_token)
|
||||
assert org_id == organization.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_api_token/1" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
|
||||
{:ok, {token, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Test Token"
|
||||
})
|
||||
|
||||
%{token: token}
|
||||
end
|
||||
|
||||
test "deletes the token", %{token: token} do
|
||||
assert {:ok, %ApiToken{}} = ApiTokens.delete_api_token(token)
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
ApiTokens.get_api_token!(token.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_api_token/2" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
|
||||
{:ok, {token, _}} =
|
||||
ApiTokens.create_api_token(%{
|
||||
organization_id: organization.id,
|
||||
user_id: user.id,
|
||||
name: "Test Token"
|
||||
})
|
||||
|
||||
%{token: token}
|
||||
end
|
||||
|
||||
test "updates the token name", %{token: token} do
|
||||
assert {:ok, updated_token} = ApiTokens.update_api_token(token, %{name: "New Name"})
|
||||
assert updated_token.name == "New Name"
|
||||
end
|
||||
|
||||
test "updates the expiration", %{token: token} do
|
||||
expires_at = DateTime.add(DateTime.utc_now(), 30, :day)
|
||||
|
||||
assert {:ok, updated_token} =
|
||||
ApiTokens.update_api_token(token, %{expires_at: expires_at})
|
||||
|
||||
assert updated_token.expires_at == DateTime.truncate(expires_at, :second)
|
||||
end
|
||||
|
||||
test "returns error with invalid name", %{token: token} do
|
||||
assert {:error, changeset} = ApiTokens.update_api_token(token, %{name: ""})
|
||||
assert "can't be blank" in errors_on(changeset).name
|
||||
end
|
||||
|
||||
test "returns error with too long name", %{token: token} do
|
||||
long_name = String.duplicate("a", 256)
|
||||
assert {:error, changeset} = ApiTokens.update_api_token(token, %{name: long_name})
|
||||
assert "should be at most 255 character(s)" in errors_on(changeset).name
|
||||
end
|
||||
end
|
||||
end
|
||||
569
test/towerops/proto/agent_test.exs
Normal file
569
test/towerops/proto/agent_test.exs
Normal file
|
|
@ -0,0 +1,569 @@
|
|||
defmodule Towerops.Agent.ProtoTest do
|
||||
@moduledoc """
|
||||
Tests for Protocol Buffer generated modules.
|
||||
|
||||
These tests verify that encoding and decoding works correctly for all
|
||||
protobuf message types used in agent communication.
|
||||
"""
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Towerops.Agent.AgentError
|
||||
alias Towerops.Agent.AgentHeartbeat
|
||||
alias Towerops.Agent.AgentJob
|
||||
alias Towerops.Agent.AgentJobList
|
||||
alias Towerops.Agent.MonitoringCheck
|
||||
alias Towerops.Agent.NeighborDiscovery
|
||||
alias Towerops.Agent.Sensor
|
||||
alias Towerops.Agent.SnmpDevice
|
||||
alias Towerops.Agent.SnmpQuery
|
||||
alias Towerops.Agent.SnmpResult
|
||||
|
||||
describe "AgentError" do
|
||||
test "encodes and decodes correctly" do
|
||||
error = %AgentError{
|
||||
device_id: "device-123",
|
||||
job_id: "job-456",
|
||||
message: "SNMP timeout",
|
||||
timestamp: 1_234_567_890
|
||||
}
|
||||
|
||||
encoded = AgentError.encode(error)
|
||||
decoded = AgentError.decode(encoded)
|
||||
|
||||
assert decoded.device_id == error.device_id
|
||||
assert decoded.job_id == error.job_id
|
||||
assert decoded.message == error.message
|
||||
assert decoded.timestamp == error.timestamp
|
||||
end
|
||||
|
||||
test "encodes empty message" do
|
||||
error = %AgentError{}
|
||||
encoded = AgentError.encode(error)
|
||||
decoded = AgentError.decode(encoded)
|
||||
|
||||
assert decoded.device_id == ""
|
||||
assert decoded.job_id == ""
|
||||
assert decoded.message == ""
|
||||
assert decoded.timestamp == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "AgentHeartbeat" do
|
||||
test "encodes and decodes correctly" do
|
||||
heartbeat = %AgentHeartbeat{
|
||||
version: "1.0.0",
|
||||
hostname: "agent-host",
|
||||
uptime_seconds: 3600,
|
||||
ip_address: "192.168.1.100"
|
||||
}
|
||||
|
||||
encoded = AgentHeartbeat.encode(heartbeat)
|
||||
decoded = AgentHeartbeat.decode(encoded)
|
||||
|
||||
assert decoded.version == heartbeat.version
|
||||
assert decoded.hostname == heartbeat.hostname
|
||||
assert decoded.uptime_seconds == heartbeat.uptime_seconds
|
||||
assert decoded.ip_address == heartbeat.ip_address
|
||||
end
|
||||
|
||||
test "encodes empty message" do
|
||||
heartbeat = %AgentHeartbeat{}
|
||||
encoded = AgentHeartbeat.encode(heartbeat)
|
||||
decoded = AgentHeartbeat.decode(encoded)
|
||||
|
||||
assert decoded.version == ""
|
||||
assert decoded.hostname == ""
|
||||
assert decoded.uptime_seconds == 0
|
||||
assert decoded.ip_address == ""
|
||||
end
|
||||
end
|
||||
|
||||
describe "JobType enum" do
|
||||
test "encodes DISCOVER constant" do
|
||||
# Enums are represented as atoms in structs but integers in the wire format
|
||||
job = %AgentJob{job_type: :DISCOVER}
|
||||
encoded = AgentJob.encode(job)
|
||||
decoded = AgentJob.decode(encoded)
|
||||
assert decoded.job_type == :DISCOVER
|
||||
end
|
||||
|
||||
test "encodes POLL constant" do
|
||||
job = %AgentJob{job_type: :POLL}
|
||||
encoded = AgentJob.encode(job)
|
||||
decoded = AgentJob.decode(encoded)
|
||||
assert decoded.job_type == :POLL
|
||||
end
|
||||
end
|
||||
|
||||
describe "QueryType enum" do
|
||||
test "encodes GET constant" do
|
||||
query = %SnmpQuery{query_type: :GET}
|
||||
encoded = SnmpQuery.encode(query)
|
||||
decoded = SnmpQuery.decode(encoded)
|
||||
assert decoded.query_type == :GET
|
||||
end
|
||||
|
||||
test "encodes WALK constant" do
|
||||
query = %SnmpQuery{query_type: :WALK}
|
||||
encoded = SnmpQuery.encode(query)
|
||||
decoded = SnmpQuery.decode(encoded)
|
||||
assert decoded.query_type == :WALK
|
||||
end
|
||||
end
|
||||
|
||||
describe "SnmpDevice" do
|
||||
test "encodes and decodes correctly" do
|
||||
device = %SnmpDevice{
|
||||
ip: "192.168.1.1",
|
||||
community: "public",
|
||||
version: "2c",
|
||||
port: 161,
|
||||
monitoring_enabled: true,
|
||||
check_interval_seconds: 60
|
||||
}
|
||||
|
||||
encoded = SnmpDevice.encode(device)
|
||||
decoded = SnmpDevice.decode(encoded)
|
||||
|
||||
assert decoded.ip == device.ip
|
||||
assert decoded.community == device.community
|
||||
assert decoded.version == device.version
|
||||
assert decoded.port == device.port
|
||||
assert decoded.monitoring_enabled == device.monitoring_enabled
|
||||
assert decoded.check_interval_seconds == device.check_interval_seconds
|
||||
end
|
||||
|
||||
test "encodes empty message" do
|
||||
device = %SnmpDevice{}
|
||||
encoded = SnmpDevice.encode(device)
|
||||
decoded = SnmpDevice.decode(encoded)
|
||||
|
||||
assert decoded.ip == ""
|
||||
assert decoded.community == ""
|
||||
assert decoded.version == ""
|
||||
assert decoded.port == 0
|
||||
assert decoded.monitoring_enabled == false
|
||||
assert decoded.check_interval_seconds == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "SnmpQuery" do
|
||||
test "encodes and decodes GET query" do
|
||||
query = %SnmpQuery{
|
||||
query_type: :GET,
|
||||
oids: ["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.2.0"]
|
||||
}
|
||||
|
||||
encoded = SnmpQuery.encode(query)
|
||||
decoded = SnmpQuery.decode(encoded)
|
||||
|
||||
assert decoded.query_type == :GET
|
||||
assert decoded.oids == query.oids
|
||||
end
|
||||
|
||||
test "encodes and decodes WALK query" do
|
||||
query = %SnmpQuery{
|
||||
query_type: :WALK,
|
||||
oids: ["1.3.6.1.2.1.2.2.1"]
|
||||
}
|
||||
|
||||
encoded = SnmpQuery.encode(query)
|
||||
decoded = SnmpQuery.decode(encoded)
|
||||
|
||||
assert decoded.query_type == :WALK
|
||||
assert decoded.oids == query.oids
|
||||
end
|
||||
|
||||
test "encodes empty message" do
|
||||
query = %SnmpQuery{}
|
||||
encoded = SnmpQuery.encode(query)
|
||||
decoded = SnmpQuery.decode(encoded)
|
||||
|
||||
assert decoded.query_type == :GET
|
||||
assert decoded.oids == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "AgentJob" do
|
||||
test "encodes and decodes DISCOVER job" do
|
||||
job = %AgentJob{
|
||||
job_id: "discover:device-123",
|
||||
job_type: :DISCOVER,
|
||||
device_id: "device-123",
|
||||
snmp_device: %SnmpDevice{
|
||||
ip: "192.168.1.1",
|
||||
community: "public",
|
||||
version: "2c",
|
||||
port: 161,
|
||||
monitoring_enabled: true,
|
||||
check_interval_seconds: 60
|
||||
},
|
||||
queries: [
|
||||
%SnmpQuery{
|
||||
query_type: :GET,
|
||||
oids: ["1.3.6.1.2.1.1.1.0"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
encoded = AgentJob.encode(job)
|
||||
decoded = AgentJob.decode(encoded)
|
||||
|
||||
assert decoded.job_id == job.job_id
|
||||
assert decoded.job_type == :DISCOVER
|
||||
assert decoded.device_id == job.device_id
|
||||
assert decoded.snmp_device.ip == job.snmp_device.ip
|
||||
assert decoded.snmp_device.community == job.snmp_device.community
|
||||
assert decoded.snmp_device.version == job.snmp_device.version
|
||||
assert decoded.snmp_device.port == job.snmp_device.port
|
||||
assert decoded.snmp_device.monitoring_enabled == job.snmp_device.monitoring_enabled
|
||||
|
||||
assert decoded.snmp_device.check_interval_seconds ==
|
||||
job.snmp_device.check_interval_seconds
|
||||
|
||||
assert length(decoded.queries) == 1
|
||||
assert hd(decoded.queries).query_type == :GET
|
||||
assert hd(decoded.queries).oids == ["1.3.6.1.2.1.1.1.0"]
|
||||
end
|
||||
|
||||
test "encodes and decodes POLL job" do
|
||||
job = %AgentJob{
|
||||
job_id: "poll:device-123",
|
||||
job_type: :POLL,
|
||||
device_id: "device-123",
|
||||
snmp_device: %SnmpDevice{
|
||||
ip: "192.168.1.1",
|
||||
community: "public",
|
||||
version: "2c",
|
||||
port: 161
|
||||
},
|
||||
queries: [
|
||||
%SnmpQuery{
|
||||
query_type: :GET,
|
||||
oids: ["1.3.6.1.4.1.14988.1.1.3.8.0"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
encoded = AgentJob.encode(job)
|
||||
decoded = AgentJob.decode(encoded)
|
||||
|
||||
assert decoded.job_id == job.job_id
|
||||
assert decoded.job_type == :POLL
|
||||
assert decoded.device_id == job.device_id
|
||||
assert decoded.snmp_device.ip == job.snmp_device.ip
|
||||
assert length(decoded.queries) == 1
|
||||
end
|
||||
|
||||
test "encodes empty message" do
|
||||
job = %AgentJob{}
|
||||
encoded = AgentJob.encode(job)
|
||||
decoded = AgentJob.decode(encoded)
|
||||
|
||||
assert decoded.job_id == ""
|
||||
assert decoded.job_type == :DISCOVER
|
||||
assert decoded.device_id == ""
|
||||
assert decoded.snmp_device == nil
|
||||
assert decoded.queries == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "AgentJobList" do
|
||||
test "encodes and decodes list of jobs" do
|
||||
job_list = %AgentJobList{
|
||||
jobs: [
|
||||
%AgentJob{
|
||||
job_id: "poll:device-1",
|
||||
job_type: :POLL,
|
||||
device_id: "device-1",
|
||||
snmp_device: %SnmpDevice{ip: "192.168.1.1"},
|
||||
queries: []
|
||||
},
|
||||
%AgentJob{
|
||||
job_id: "discover:device-2",
|
||||
job_type: :DISCOVER,
|
||||
device_id: "device-2",
|
||||
snmp_device: %SnmpDevice{ip: "192.168.1.2"},
|
||||
queries: []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
encoded = AgentJobList.encode(job_list)
|
||||
decoded = AgentJobList.decode(encoded)
|
||||
|
||||
assert length(decoded.jobs) == 2
|
||||
assert Enum.at(decoded.jobs, 0).job_id == "poll:device-1"
|
||||
assert Enum.at(decoded.jobs, 0).job_type == :POLL
|
||||
assert Enum.at(decoded.jobs, 1).job_id == "discover:device-2"
|
||||
assert Enum.at(decoded.jobs, 1).job_type == :DISCOVER
|
||||
end
|
||||
|
||||
test "encodes empty job list" do
|
||||
job_list = %AgentJobList{jobs: []}
|
||||
encoded = AgentJobList.encode(job_list)
|
||||
decoded = AgentJobList.decode(encoded)
|
||||
|
||||
assert decoded.jobs == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "SnmpResult" do
|
||||
test "encodes and decodes with oid_values map" do
|
||||
result = %SnmpResult{
|
||||
device_id: "device-123",
|
||||
job_type: :POLL,
|
||||
oid_values: %{
|
||||
"1.3.6.1.2.1.1.1.0" => "Cisco IOS",
|
||||
"1.3.6.1.2.1.1.3.0" => "12345"
|
||||
},
|
||||
timestamp: 1_234_567_890
|
||||
}
|
||||
|
||||
encoded = SnmpResult.encode(result)
|
||||
decoded = SnmpResult.decode(encoded)
|
||||
|
||||
assert decoded.device_id == result.device_id
|
||||
assert decoded.job_type == :POLL
|
||||
assert decoded.oid_values == result.oid_values
|
||||
assert decoded.timestamp == result.timestamp
|
||||
end
|
||||
|
||||
test "encodes and decodes DISCOVER result" do
|
||||
result = %SnmpResult{
|
||||
device_id: "device-123",
|
||||
job_type: :DISCOVER,
|
||||
oid_values: %{},
|
||||
timestamp: 1_234_567_890
|
||||
}
|
||||
|
||||
encoded = SnmpResult.encode(result)
|
||||
decoded = SnmpResult.decode(encoded)
|
||||
|
||||
assert decoded.device_id == result.device_id
|
||||
assert decoded.job_type == :DISCOVER
|
||||
assert decoded.timestamp == result.timestamp
|
||||
end
|
||||
|
||||
test "encodes empty message" do
|
||||
result = %SnmpResult{}
|
||||
encoded = SnmpResult.encode(result)
|
||||
decoded = SnmpResult.decode(encoded)
|
||||
|
||||
assert decoded.device_id == ""
|
||||
assert decoded.job_type == :DISCOVER
|
||||
assert decoded.oid_values == %{}
|
||||
assert decoded.timestamp == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "SnmpResult.OidValuesEntry" do
|
||||
test "encodes and decodes map entry" do
|
||||
entry = %SnmpResult.OidValuesEntry{
|
||||
key: "1.3.6.1.2.1.1.1.0",
|
||||
value: "System Description"
|
||||
}
|
||||
|
||||
encoded = SnmpResult.OidValuesEntry.encode(entry)
|
||||
decoded = SnmpResult.OidValuesEntry.decode(encoded)
|
||||
|
||||
assert decoded.key == entry.key
|
||||
assert decoded.value == entry.value
|
||||
end
|
||||
end
|
||||
|
||||
describe "MonitoringCheck" do
|
||||
test "encodes and decodes success check" do
|
||||
check = %MonitoringCheck{
|
||||
device_id: "device-123",
|
||||
status: "success",
|
||||
response_time_ms: 45.2,
|
||||
timestamp: 1_234_567_890
|
||||
}
|
||||
|
||||
encoded = MonitoringCheck.encode(check)
|
||||
decoded = MonitoringCheck.decode(encoded)
|
||||
|
||||
assert decoded.device_id == check.device_id
|
||||
assert decoded.status == check.status
|
||||
assert decoded.response_time_ms == check.response_time_ms
|
||||
assert decoded.timestamp == check.timestamp
|
||||
end
|
||||
|
||||
test "encodes and decodes failure check" do
|
||||
check = %MonitoringCheck{
|
||||
device_id: "device-123",
|
||||
status: "failure",
|
||||
response_time_ms: 0.0,
|
||||
timestamp: 1_234_567_890
|
||||
}
|
||||
|
||||
encoded = MonitoringCheck.encode(check)
|
||||
decoded = MonitoringCheck.decode(encoded)
|
||||
|
||||
assert decoded.device_id == check.device_id
|
||||
assert decoded.status == check.status
|
||||
assert decoded.response_time_ms == check.response_time_ms
|
||||
assert decoded.timestamp == check.timestamp
|
||||
end
|
||||
|
||||
test "encodes empty message" do
|
||||
check = %MonitoringCheck{}
|
||||
encoded = MonitoringCheck.encode(check)
|
||||
decoded = MonitoringCheck.decode(encoded)
|
||||
|
||||
assert decoded.device_id == ""
|
||||
assert decoded.status == ""
|
||||
assert decoded.response_time_ms == 0.0
|
||||
assert decoded.timestamp == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "NeighborDiscovery" do
|
||||
test "encodes and decodes LLDP neighbor" do
|
||||
neighbor = %NeighborDiscovery{
|
||||
interface_id: "interface-123",
|
||||
protocol: "LLDP",
|
||||
remote_chassis_id: "00:11:22:33:44:55",
|
||||
remote_system_name: "neighbor-switch",
|
||||
remote_system_description: "Cisco IOS",
|
||||
remote_platform: "cisco",
|
||||
remote_port_id: "GigabitEthernet1/0/1",
|
||||
remote_port_description: "Uplink to Core",
|
||||
remote_address: "192.168.1.2",
|
||||
remote_capabilities: ["bridge", "router"],
|
||||
timestamp: 1_234_567_890
|
||||
}
|
||||
|
||||
encoded = NeighborDiscovery.encode(neighbor)
|
||||
decoded = NeighborDiscovery.decode(encoded)
|
||||
|
||||
assert decoded.interface_id == neighbor.interface_id
|
||||
assert decoded.protocol == neighbor.protocol
|
||||
assert decoded.remote_chassis_id == neighbor.remote_chassis_id
|
||||
assert decoded.remote_system_name == neighbor.remote_system_name
|
||||
assert decoded.remote_system_description == neighbor.remote_system_description
|
||||
assert decoded.remote_platform == neighbor.remote_platform
|
||||
assert decoded.remote_port_id == neighbor.remote_port_id
|
||||
assert decoded.remote_port_description == neighbor.remote_port_description
|
||||
assert decoded.remote_address == neighbor.remote_address
|
||||
assert decoded.remote_capabilities == neighbor.remote_capabilities
|
||||
assert decoded.timestamp == neighbor.timestamp
|
||||
end
|
||||
|
||||
test "encodes and decodes CDP neighbor" do
|
||||
neighbor = %NeighborDiscovery{
|
||||
interface_id: "interface-456",
|
||||
protocol: "CDP",
|
||||
remote_chassis_id: "Router-2",
|
||||
remote_system_name: "core-router",
|
||||
remote_platform: "Cisco 2960",
|
||||
remote_port_id: "FastEthernet0/1",
|
||||
timestamp: 1_234_567_890
|
||||
}
|
||||
|
||||
encoded = NeighborDiscovery.encode(neighbor)
|
||||
decoded = NeighborDiscovery.decode(encoded)
|
||||
|
||||
assert decoded.interface_id == neighbor.interface_id
|
||||
assert decoded.protocol == neighbor.protocol
|
||||
assert decoded.remote_chassis_id == neighbor.remote_chassis_id
|
||||
assert decoded.timestamp == neighbor.timestamp
|
||||
end
|
||||
|
||||
test "encodes empty message" do
|
||||
neighbor = %NeighborDiscovery{}
|
||||
encoded = NeighborDiscovery.encode(neighbor)
|
||||
decoded = NeighborDiscovery.decode(encoded)
|
||||
|
||||
assert decoded.interface_id == ""
|
||||
assert decoded.protocol == ""
|
||||
assert decoded.remote_chassis_id == ""
|
||||
assert decoded.remote_system_name == ""
|
||||
assert decoded.remote_capabilities == []
|
||||
assert decoded.timestamp == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "Sensor.MetadataEntry" do
|
||||
test "encodes and decodes map entry" do
|
||||
entry = %Sensor.MetadataEntry{
|
||||
key: "location",
|
||||
value: "CPU Core 1"
|
||||
}
|
||||
|
||||
encoded = Sensor.MetadataEntry.encode(entry)
|
||||
decoded = Sensor.MetadataEntry.decode(encoded)
|
||||
|
||||
assert decoded.key == entry.key
|
||||
assert decoded.value == entry.value
|
||||
end
|
||||
|
||||
test "encodes empty entry" do
|
||||
entry = %Sensor.MetadataEntry{}
|
||||
encoded = Sensor.MetadataEntry.encode(entry)
|
||||
decoded = Sensor.MetadataEntry.decode(encoded)
|
||||
|
||||
assert decoded.key == ""
|
||||
assert decoded.value == ""
|
||||
end
|
||||
end
|
||||
|
||||
describe "Sensor (protobuf)" do
|
||||
test "encodes and decodes sensor with metadata" do
|
||||
sensor = %Sensor{
|
||||
id: "sensor-123",
|
||||
type: "temperature",
|
||||
oid: "1.3.6.1.4.1.14988.1.1.3.10.0",
|
||||
divisor: 10.0,
|
||||
unit: "celsius",
|
||||
metadata: %{
|
||||
"location" => "CPU Core 1",
|
||||
"threshold" => "80"
|
||||
}
|
||||
}
|
||||
|
||||
encoded = Sensor.encode(sensor)
|
||||
decoded = Sensor.decode(encoded)
|
||||
|
||||
assert decoded.id == sensor.id
|
||||
assert decoded.type == sensor.type
|
||||
assert decoded.oid == sensor.oid
|
||||
assert decoded.divisor == sensor.divisor
|
||||
assert decoded.unit == sensor.unit
|
||||
assert decoded.metadata == sensor.metadata
|
||||
end
|
||||
|
||||
test "encodes and decodes sensor without metadata" do
|
||||
sensor = %Sensor{
|
||||
id: "sensor-456",
|
||||
type: "voltage",
|
||||
oid: "1.3.6.1.4.1.14988.1.1.3.8.0",
|
||||
divisor: 10.0,
|
||||
unit: "volts"
|
||||
}
|
||||
|
||||
encoded = Sensor.encode(sensor)
|
||||
decoded = Sensor.decode(encoded)
|
||||
|
||||
assert decoded.id == sensor.id
|
||||
assert decoded.type == sensor.type
|
||||
assert decoded.oid == sensor.oid
|
||||
assert decoded.divisor == sensor.divisor
|
||||
assert decoded.unit == sensor.unit
|
||||
end
|
||||
|
||||
test "encodes empty message" do
|
||||
sensor = %Sensor{}
|
||||
encoded = Sensor.encode(sensor)
|
||||
decoded = Sensor.decode(encoded)
|
||||
|
||||
assert decoded.id == ""
|
||||
assert decoded.type == ""
|
||||
assert decoded.oid == ""
|
||||
assert decoded.divisor == 0.0
|
||||
assert decoded.unit == ""
|
||||
assert decoded.metadata == %{}
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue