Skip to content
This repository has been archived by the owner on Oct 30, 2023. It is now read-only.

Commit

Permalink
feat: satisfactory test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
madclaws committed Oct 29, 2023
1 parent 45e91e8 commit f6ddfd8
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 13 deletions.
12 changes: 9 additions & 3 deletions lib/ex_ucan.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,25 @@ defmodule ExUcan do
Keypair.create()
end

# TODO: docs
@doc """
Signs the payload with keypair and returns a UCAN struct
- payload - Ucan payload type
- keypair - A Keymaterial implemented struct
"""
@spec sign(payload :: UcanPayload.t(), keypair :: struct()) :: Ucan.t()
def sign(payload, keypair) do
Token.sign_with_payload(payload, keypair)
end

# TODO: to be removed
@doc """
Encode the Ucan.t() struct to JWT like token
"""
@spec encode(Ucan.t()) :: String.t()
def encode(ucan) do
Token.encode(ucan)
end

# TODO: Test this after Builder is setup
@doc """
Validate the UCAN token's signature and timestamps
Expand Down
24 changes: 21 additions & 3 deletions lib/ex_ucan/builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ defmodule ExUcan.Builder do
lifetime: number(),
expiration: number(),
not_before: number(),
# Add Facts struct later
facts: map(),
proofs: list(String.t()),
add_nonce?: boolean()
Expand Down Expand Up @@ -143,18 +142,37 @@ defmodule ExUcan.Builder do
builder
end

# TODO: docs
@doc """
Builds the UCAN `payload` from the `Builder` workflow
A runtime exception is raised if build payloads fails.
A sample builder workflow to create ucan payload
```Elixir
alias ExUcan.Builder
keypair = ExUcan.create_default_keypair()
ExUcan.Builder.default
|> Builder.issued_by(keypair)
|> Builder.for_audience("did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2N")
|> Builder.with_lifetime(864000)
|> Builder.build!
```
"""
@spec build!(__MODULE__.t()) :: String.t()
@spec build!(__MODULE__.t()) :: UcanPayload.t()
def build!(builder) do
case Token.build_payload(builder) do
{:ok, payload} -> payload
{:error, err} -> raise err
end
end

@doc """
Builds the UCAN `payload` from the `Builder` struct
An error tuple with reason is returned if build payloads fails.
"""
@spec build(__MODULE__.t()) :: {:ok, UcanPayload.t()} | {:error, String.t()}
def build(builder) do
Token.build_payload(builder)
Expand Down
32 changes: 25 additions & 7 deletions lib/ex_ucan/core/token.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ defmodule ExUcan.Core.Token do
@moduledoc """
Creates and manages UCAN tokens
"""
alias ExUcan.Keymaterial.Ed25519.Keypair
alias ExUcan.Core.Utils
alias ExUcan.Builder
alias ExUcan.Keymaterial.Ed25519.Crypto
alias ExUcan.Core.Structs.UcanHeader
Expand All @@ -12,11 +14,17 @@ defmodule ExUcan.Core.Token do
@token_type "JWT"
@version %{major: 0, minor: 10, patch: 0}

# TODO: docs
@spec build_payload(params :: Builder) :: {:ok, UcanPayload.t()} | {:error, String.t()}
@doc """
Takes a UcanBuilder and generates a UCAN payload
Returns an error tuple with reason, if failed to generate payload
"""
@spec build_payload(params :: Builder.t()) :: {:ok, UcanPayload.t()} | {:error, String.t()}
def build_payload(%Builder{issuer: nil}), do: {:error, "must call issued_by/2"}
def build_payload(%Builder{audience: nil}), do: {:error, "must call for_audience/2"}
def build_payload(%Builder{lifetime: nil}), do: {:error, "must call with_lifetime/2"}

def build_payload(%Builder{lifetime: life, expiration: exp}) when life == nil and exp == nil,
do: {:error, "must call with_lifetime/2 or with_expiration/2"}

def build_payload(params) do
did = Keymaterial.get_did(params.issuer)
Expand All @@ -27,13 +35,13 @@ defmodule ExUcan.Core.Token do
exp = params.expiration || current_time_in_seconds + params.lifetime

{:ok,
%{
%UcanPayload{
ucv: "#{@version.major}.#{@version.minor}.#{@version.patch}",
iss: did,
aud: params.audience,
nbf: params.not_before,
exp: exp,
nnc: params.add_nonce?,
nnc: add_nonce(params.add_nonce? || false),
fct: params.facts,
cap: params.capabilities,
prf: params.proofs
Expand Down Expand Up @@ -70,7 +78,13 @@ defmodule ExUcan.Core.Token do
end
end

@spec sign_with_payload(payload :: UcanPayload.t(), keypair :: struct()) :: Ucan.t()
@doc """
Signs the payload with keypair and returns a UCAN struct
- payload - Ucan payload type
- keypair - A Keymaterial implemented struct
"""
@spec sign_with_payload(payload :: UcanPayload.t(), keypair :: Keypair.t()) :: Ucan.t()
def sign_with_payload(payload, keypair) do
header = %UcanHeader{alg: keypair.jwt_alg, typ: @token_type}
encoded_header = encode_ucan_parts(header)
Expand Down Expand Up @@ -101,6 +115,7 @@ defmodule ExUcan.Core.Token do

@spec is_too_early?(UcanPayload.t()) :: boolean()
defp is_too_early?(%UcanPayload{nbf: nil}), do: false

defp is_too_early?(%UcanPayload{nbf: nbf}) do
nbf > DateTime.utc_now() |> DateTime.to_unix()
end
Expand Down Expand Up @@ -134,7 +149,7 @@ defmodule ExUcan.Core.Token do
end

@spec verify_signature(String.t(), String.t(), String.t()) :: :ok | {:error, String.t()}
def verify_signature(did, data, signature) do
defp verify_signature(did, data, signature) do
with {:ok, public_key} <- Crypto.did_to_publickey(did),
true <- :public_key.verify(data, :ignored, signature, {:ed_pub, :ed25519, public_key}) do
:ok
Expand All @@ -143,4 +158,7 @@ defmodule ExUcan.Core.Token do
err -> err
end
end

defp add_nonce(true), do: Utils.generate_nonce()
defp add_nonce(false), do: nil
end
5 changes: 5 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ defmodule ExUcan.MixProject do
test_coverage: [
summary: [
threshold: 80
],
ignore_modules: [
ExUcan.Core.Structs.Ucan,
ExUcan.Core.Structs.UcanHeader,
ExUcan.Core.Structs.UcanPayload
]
]
]
Expand Down
121 changes: 121 additions & 0 deletions test/builder_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
defmodule BuilderTest do
alias ExUcan.Core.Capability
alias ExUcan.Core.Structs.UcanPayload
alias ExUcan.Builder
use ExUnit.Case

setup do
keypair = ExUcan.create_default_keypair()
%{keypair: keypair}
end

@tag :build
test "builder functions, default" do
assert %Builder{issuer: nil} = Builder.default()
end

@tag :build
test "non-working builder flow", _meta do
assert {:error, "must call issued_by/2"} = Builder.default() |> Builder.build()
end

@tag :build
test "non-working builder flow, need for_audience", meta do
assert {:error, "must call for_audience/2"} =
Builder.default()
|> Builder.issued_by(meta.keypair)
|> Builder.build()
end

@tag :build
test "non-working builder flow, need with_lifetime", meta do
assert {:error, "must call with_lifetime/2 or with_expiration/2"} =
Builder.default()
|> Builder.issued_by(meta.keypair)
|> Builder.for_audience("did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD")
|> Builder.build()
end

@tag :build
test "working builder flow", meta do
assert {:ok, %UcanPayload{}} =
Builder.default()
|> Builder.issued_by(meta.keypair)
|> Builder.for_audience("did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD")
|> Builder.with_lifetime(86_400)
|> Builder.build()
end

@tag :build
test "working builder flow, with expiration", meta do
assert {:ok, %UcanPayload{}} =
Builder.default()
|> Builder.issued_by(meta.keypair)
|> Builder.for_audience("did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD")
|> Builder.with_expiration((DateTime.utc_now() |> DateTime.to_unix()) + 86_400)
|> Builder.build()
end

@tag :build
test "more workflows", meta do
assert {:ok, %UcanPayload{fct: %{"door" => "bronze"}, nnc: nnc}} =
Builder.default()
|> Builder.issued_by(meta.keypair)
|> Builder.for_audience("did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD")
|> Builder.with_expiration((DateTime.utc_now() |> DateTime.to_unix()) + 86_400)
|> Builder.with_fact("door", "bronze")
|> Builder.with_nonce()
|> Builder.not_before((DateTime.utc_now() |> DateTime.to_unix()) - 86_400)
|> Builder.build()

assert is_binary(nnc)
end

@tag :build
test "with capabilities", meta do
cap = Capability.new("example://bar", "ability/bar", %{"beep" => 1})

assert {:ok, %UcanPayload{cap: caps}} =
Builder.default()
|> Builder.issued_by(meta.keypair)
|> Builder.for_audience("did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD")
|> Builder.with_expiration((DateTime.utc_now() |> DateTime.to_unix()) + 86_400)
|> Builder.claiming_capability(cap)
|> Builder.build()

assert [%{resource: "example://bar", ability: "ability/bar"} | _] = caps
end

@tag :build
test "with bang variant success", meta do
cap = Capability.new("example://bar", "ability/bar", %{"beep" => 1})

assert %UcanPayload{cap: caps} =
Builder.default()
|> Builder.issued_by(meta.keypair)
|> Builder.for_audience("did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD")
|> Builder.with_expiration((DateTime.utc_now() |> DateTime.to_unix()) + 86_400)
|> Builder.claiming_capability(cap)
|> Builder.build!()

assert [%{resource: "example://bar", ability: "ability/bar"} | _] = caps
end

@tag :build
test "with bang variant fail", meta do
cap = Capability.new("example://bar", "ability/bar", %{"beep" => 1})

res =
try do
Builder.default()
|> Builder.issued_by(meta.keypair)
|> Builder.for_audience("did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD")
|> Builder.claiming_capability(cap)
|> Builder.build!()
rescue
e in RuntimeError -> e
end

assert %RuntimeError{message: _} = res
end
end
49 changes: 49 additions & 0 deletions test/ex_ucan_test.exs
Original file line number Diff line number Diff line change
@@ -1,11 +1,60 @@
defmodule ExUcanTest do
alias ExUcan.Builder
alias ExUcan.Keymaterial.Ed25519.Keypair
use ExUnit.Case
doctest ExUcan

setup do
keypair = ExUcan.create_default_keypair()
%{keypair: keypair}
end

test "create_default_keypair" do
assert %Keypair{jwt_alg: "EdDSA"} = keypair = Keypair.create()
assert is_binary(keypair.public_key)
assert is_binary(keypair.secret_key)
end

@tag :exucan
test "validate_token, success", meta do
token =
Builder.default()
|> Builder.issued_by(meta.keypair)
|> Builder.for_audience("did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD")
|> Builder.with_expiration((DateTime.utc_now() |> DateTime.to_unix()) + 86_400)
|> Builder.build!()
|> ExUcan.sign(meta.keypair)
|> ExUcan.encode()

assert :ok = ExUcan.validate_token(token)
end

@tag :exucan
test "invalid token, due to expiry", meta do
token =
Builder.default()
|> Builder.issued_by(meta.keypair)
|> Builder.for_audience("did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD")
|> Builder.with_expiration((DateTime.utc_now() |> DateTime.to_unix()) - 5)
|> Builder.build!()
|> ExUcan.sign(meta.keypair)
|> ExUcan.encode()

assert {:error, "Ucan token is already expired"} = ExUcan.validate_token(token)
end

@tag :exucan
test "invalid token, too early", meta do
token =
Builder.default()
|> Builder.issued_by(meta.keypair)
|> Builder.for_audience("did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD")
|> Builder.with_expiration((DateTime.utc_now() |> DateTime.to_unix()) + 86_400)
|> Builder.not_before((DateTime.utc_now() |> DateTime.to_unix()) + (div(86400, 2)))
|> Builder.build!()
|> ExUcan.sign(meta.keypair)
|> ExUcan.encode()

assert {:error, "Ucan token is not yet active"} = ExUcan.validate_token(token)
end
end

0 comments on commit f6ddfd8

Please sign in to comment.