diff --git a/.formatter.exs b/.formatter.exs index 935d310..1371f49 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,5 +1,5 @@ # Used by "mix format" [ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], - plugins: [Styler] + plugins: [DoctestFormatter, Styler] ] diff --git a/lib/mix/tasks/mix_dependency_submission.ex b/lib/mix/tasks/mix_dependency_submission.ex index f1914e2..d23043b 100644 --- a/lib/mix/tasks/mix_dependency_submission.ex +++ b/lib/mix/tasks/mix_dependency_submission.ex @@ -3,7 +3,12 @@ if Mix.env() == :dev do defmodule Mix.Tasks.MixDependencySubmission do @shortdoc "Run mix_depdendency_submission" - @moduledoc @shortdoc + @moduledoc """ + #{@shortdoc} + + Only intented for development purposes. Use the burrito binary for + production. + """ use Mix.Task @@ -11,6 +16,7 @@ if Mix.env() == :dev do @requirements ["app.start"] + @doc false @impl Mix.Task def run(args) do Submit.run(args) diff --git a/lib/mix_dependency_submission.ex b/lib/mix_dependency_submission.ex index 31fe62c..3c003e9 100644 --- a/lib/mix_dependency_submission.ex +++ b/lib/mix_dependency_submission.ex @@ -1,11 +1,94 @@ defmodule MixDependencySubmission do - @moduledoc false + @moduledoc """ + Builds the dependency submission payload from one or more Mix projects. + + Responsible for collecting dependency data, resolving manifests, and + assembling the top-level submission struct to be sent to GitHub. + + See https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28#create-a-snapshot-of-dependencies-for-a-repository + + > #### API Interface {: .warning} + > This project is **not a library** and is intended **only** for use as a + > GitHub Action. All modules, functions, and types are considered internal and + > may change at any time without notice. + > + > If you're looking to integrate this functionality into your own tooling, open + > an issue to discuss the use case, but do not rely on this as a stable library + > interface. + + """ alias MixDependencySubmission.Fetcher alias MixDependencySubmission.Submission alias MixDependencySubmission.Submission.Manifest alias MixDependencySubmission.Util + @doc """ + Builds a new `MixDependencySubmission.Submission` from a set of project + options. + + Finds nested Mix projects (unless ignored), resolves dependencies, + and prepares the manifest payload. + + ## Examples + + iex> MixDependencySubmission.submission( + ...> github_job_id: "job123", + ...> github_workflow: "ci.yml", + ...> sha: "sha", + ...> ref: "refs/heads/main", + ...> project_path: ".", + ...> install_deps?: false, + ...> ignore: [] + ...> ) + %MixDependencySubmission.Submission{ + version: 0, + job: %MixDependencySubmission.Submission.Job{ + id: "job123", + correlator: "ci.yml job123", + html_url: nil + }, + sha: "sha", + ref: "refs/heads/main", + detector: %MixDependencySubmission.Submission.Detector{ + name: "mix_dependency_submission", + version: %Version{major: 1, minor: 0, patch: 0, pre: ["beta", 8]}, + url: %URI{ + scheme: "https", + userinfo: nil, + host: "github.com", + port: 443, + path: "/erlef/mix-dependency-submission", + query: nil, + fragment: nil + } + }, + scanned: ~U[2025-04-19 10:15:11.656801Z], + metadata: %{}, + manifests: %{ + "mix.exs" => %MixDependencySubmission.Submission.Manifest{ + name: "mix.exs", + file: %MixDependencySubmission.Submission.Manifest.File{ + source_location: "mix.exs" + }, + metadata: %{}, + resolved: %{ + "expo" => %MixDependencySubmission.Submission.Manifest.Dependency{ + package_url: + Purl.parse!( + "pkg:github/elixir-gettext/expo@2ae85019d62288001bdc4a949d65bf650beee315" + ), + metadata: %{}, + relationship: :direct, + scope: :runtime, + dependencies: [] + } + } + } + } + } + + """ @spec submission( options :: [ {:github_job_id, String.t()} @@ -33,7 +116,9 @@ defmodule MixDependencySubmission do options[:project_path] |> find_mix_projects(options[:ignore] || [], options[:paths_relative_to]) |> Map.new(fn project_path -> - manifest = manifest(project_path, Keyword.take(options, [:paths_relative_to, :install_deps?])) + manifest = + manifest(project_path, Keyword.take(options, [:paths_relative_to, :install_deps?])) + {manifest.file.source_location, manifest} end) @@ -46,7 +131,15 @@ defmodule MixDependencySubmission do }) end - @spec manifest(project_path :: Path.t(), options :: [{:paths_relative_to, Path.t()} | {:install_deps?, boolean()}]) :: + @doc """ + Resolves the dependency manifest for a single Mix project. + + Optionally installs dependencies beforehand if `install_deps?` is true. + """ + @spec manifest( + project_path :: Path.t(), + options :: [{:paths_relative_to, Path.t()} | {:install_deps?, boolean()}] + ) :: Manifest.t() def manifest(project_path, options) do Util.in_project(project_path, fn _mix_module -> @@ -58,7 +151,11 @@ defmodule MixDependencySubmission do end) end - @spec find_mix_projects(project_path :: Path.t(), ignore :: [Path.t()], paths_relative_to :: Path.t()) :: [Path.t()] + @spec find_mix_projects( + project_path :: Path.t(), + ignore :: [Path.t()], + paths_relative_to :: Path.t() + ) :: [Path.t()] defp find_mix_projects(project_path, ignore, paths_relative_to) do ignore = Enum.map(ignore, &Path.expand(&1, paths_relative_to)) @@ -102,6 +199,7 @@ defmodule MixDependencySubmission do } end - @spec drop_empty(map :: %{key => value | nil}) :: %{key => value} when key: term(), value: term() + @spec drop_empty(map :: %{key => value | nil}) :: %{key => value} + when key: term(), value: term() defp drop_empty(map), do: map |> Enum.reject(fn {_key, value} -> value in [nil, ""] end) |> Map.new() end diff --git a/lib/mix_dependency_submission/api_client.ex b/lib/mix_dependency_submission/api_client.ex index 385b897..05fdc16 100644 --- a/lib/mix_dependency_submission/api_client.ex +++ b/lib/mix_dependency_submission/api_client.ex @@ -1,7 +1,44 @@ defmodule MixDependencySubmission.ApiClient do - @moduledoc false + @moduledoc """ + Handles submission of the dependency snapshot to the GitHub Dependency Submission API. + """ + alias MixDependencySubmission.Submission + @doc """ + Submits a dependency snapshot to the GitHub API. + + Returns `{:ok, response}` if the submission was accepted, or + `{:error, {:unexpected_response, response}}` for other HTTP status codes. + + ## Examples + + iex> submission = %MixDependencySubmission.Submission{ + ...> version: 0, + ...> job: %MixDependencySubmission.Submission.Job{ + ...> id: "job123", + ...> correlator: "workflow job123" + ...> }, + ...> sha: String.duplicate("a", 40), + ...> ref: "refs/heads/main", + ...> detector: %MixDependencySubmission.Submission.Detector{ + ...> name: "example", + ...> version: Version.parse!("1.0.0"), + ...> url: URI.parse("https://example.com") + ...> }, + ...> scanned: DateTime.utc_now(), + ...> manifests: %{} + ...> } + ...> + ...> {:ok, %Req.Response{} = response} = + ...> MixDependencySubmission.ApiClient.submit( + ...> submission, + ...> "https://api.github.com", + ...> "owner/repo", + ...> "ghp_exampletoken" + ...> ) + + """ @spec submit( submission :: Submission.t(), github_api_url :: String.t(), diff --git a/lib/mix_dependency_submission/cli.ex b/lib/mix_dependency_submission/cli.ex index bfa6f56..6d5a2b3 100644 --- a/lib/mix_dependency_submission/cli.ex +++ b/lib/mix_dependency_submission/cli.ex @@ -1,9 +1,29 @@ defmodule MixDependencySubmission.CLI do - @moduledoc false + @moduledoc """ + Handles parsing of CLI arguments using `Optimus`. + + Used to configure and validate inputs for submitting a dependency snapshot. + """ + @app Mix.Project.config()[:app] @description Mix.Project.config()[:description] @version Mix.Project.config()[:version] + @doc """ + Parses CLI arguments and returns the parsed result. + + Raises on invalid input. + + ## Examples + + iex> argv = + ...> ~w[--github-repository org/repo --github-job-id 123 --github-workflow build --sha sha --ref refs/heads/main --github-token ghp_xxx] + ...> + ...> result = MixDependencySubmission.CLI.parse!(argv) + ...> result.options.github_repository + "org/repo" + + """ @spec parse!([String.t()]) :: Optimus.ParseResult.t() def parse!(argv) do cli_definition() diff --git a/lib/mix_dependency_submission/cli/submit.ex b/lib/mix_dependency_submission/cli/submit.ex index d5c9b81..d0b6be5 100644 --- a/lib/mix_dependency_submission/cli/submit.ex +++ b/lib/mix_dependency_submission/cli/submit.ex @@ -1,10 +1,51 @@ defmodule MixDependencySubmission.CLI.Submit do - @moduledoc false + @moduledoc """ + Handles the CLI submit command for Mix Dependency Submission. + + This module parses CLI arguments, builds the dependency submission payload, + and sends it to the GitHub Dependency Submission API. It logs relevant details + about the submission process and handles success or failure scenarios + accordingly. + """ + alias MixDependencySubmission.ApiClient alias MixDependencySubmission.CLI require Logger + @doc """ + Parses command-line arguments and submits the dependency snapshot to the + GitHub API. + + This function is intended to be called from the CLI. It: + + - Parses CLI arguments using `Optimus`. + - Generates a dependency submission using + `MixDependencySubmission.submission/1`. + - Logs the resulting submission in pretty-printed JSON. + - Sends the submission to GitHub using + `MixDependencySubmission.ApiClient.submit/4`. + - Logs the response or error and exits with code 0 or 1 accordingly. + + ## Parameters + + - `argv`: A list of command-line argument strings. + + ## Behavior + + This function does not return. It will halt or stop the system depending on + the outcome of the submission. + + ## Examples + + iex> MixDependencySubmission.CLI.Submit.run([ + ...> "--project-path", + ...> ".", + ...> "--github-repository", + ...> "org/repo" + ...> ]) + + """ @spec run(argv :: [String.t()]) :: no_return() def run(argv) do %Optimus.ParseResult{ diff --git a/lib/mix_dependency_submission/fetcher.ex b/lib/mix_dependency_submission/fetcher.ex index a0440fa..01f557a 100644 --- a/lib/mix_dependency_submission/fetcher.ex +++ b/lib/mix_dependency_submission/fetcher.ex @@ -1,6 +1,14 @@ defmodule MixDependencySubmission.Fetcher do @moduledoc """ - Behaviour for Manifest Fetchers + Defines the behaviour for manifest fetchers and provides an entry point to collect + dependency data from multiple sources. + + The built-in fetchers include: + + * `MixDependencySubmission.Fetcher.MixFile` — parses `mix.exs` + * `MixDependencySubmission.Fetcher.MixLock` — parses `mix.lock` + * `MixDependencySubmission.Fetcher.MixRuntime` — inspects runtime dependency + graph """ alias MixDependencySubmission.SCM @@ -20,10 +28,53 @@ defmodule MixDependencySubmission.Fetcher do optional(:mix_config) => Keyword.t() } + @doc """ + Fetches dependencies from a specific source. + + Implementers must return a map of app names to raw dependency data, or `nil` + if no data is available. + + This callback is used by the main `MixDependencySubmission.Fetcher.fetch/0` + function to gather and merge data from multiple sources like `mix.exs`, + `mix.lock`, or the compiled dependency graph. + + ## Example return value + + %{ + my_dep: %{ + scm: Mix.SCM.Hex, + version: "0.1.0", + scope: :runtime, + relationship: :direct + } + } + + Returning `nil` signals that no data could be fetched by the implementation. + """ @callback fetch() :: %{optional(app_name()) => dependency()} | nil @manifest_fetchers [__MODULE__.MixFile, __MODULE__.MixLock, __MODULE__.MixRuntime] + @doc """ + Fetches and merges dependencies from all registered fetchers. + + Returns a map of stringified app names to structured `Dependency` records, + ready to be submitted as a manifest. + + ## Examples + + iex> %{ + ...> "burrito" => %MixDependencySubmission.Submission.Manifest.Dependency{ + ...> package_url: %Purl{type: "hex", name: "burrito"}, + ...> metadata: %{}, + ...> relationship: :direct, + ...> scope: :runtime, + ...> dependencies: _dependencies + ...> } + ...> } = MixDependencySubmission.Fetcher.fetch() + + Note: This test assumes an Elixir project that is currently loaded. + """ @spec fetch() :: %{String.t() => Dependency.t()} | nil def fetch do @manifest_fetchers @@ -44,7 +95,9 @@ defmodule MixDependencySubmission.Fetcher do @spec merge(app_name(), left :: dependency(), right :: dependency()) :: dependency() defp merge(_app, left, right), do: Map.merge(left, right) - @spec transform_all(dependencies :: %{app_name() => dependency()}) :: %{String.t() => Dependency.t()} + @spec transform_all(dependencies :: %{app_name() => dependency()}) :: %{ + String.t() => Dependency.t() + } defp transform_all(dependencies) do dependencies = Map.new(dependencies, fn {app, dependency} -> @@ -66,7 +119,8 @@ defmodule MixDependencySubmission.Fetcher do @spec transform(app_name(), dependency()) :: Dependency.t() defp transform(app, dependency) do - sub_dependencies = Enum.uniq((dependency[:dependencies] || []) ++ lock_dependencies(dependency)) + sub_dependencies = + Enum.uniq((dependency[:dependencies] || []) ++ lock_dependencies(dependency)) metadata = %{ @@ -139,6 +193,7 @@ defmodule MixDependencySubmission.Fetcher do defp lock_dependencies(_dependency), do: [] - @spec drop_empty(map :: %{key => value | nil}) :: %{key => value} when key: term(), value: term() + @spec drop_empty(map :: %{key => value | nil}) :: %{key => value} + when key: term(), value: term() defp drop_empty(map), do: map |> Enum.reject(fn {_key, value} -> value in [nil, ""] end) |> Map.new() end diff --git a/lib/mix_dependency_submission/fetcher/mix_file.ex b/lib/mix_dependency_submission/fetcher/mix_file.ex index 2b761bc..a8550f4 100644 --- a/lib/mix_dependency_submission/fetcher/mix_file.ex +++ b/lib/mix_dependency_submission/fetcher/mix_file.ex @@ -1,12 +1,38 @@ defmodule MixDependencySubmission.Fetcher.MixFile do @moduledoc """ - Fetch Dependencies from mix.exs directly. + A `MixDependencySubmission.Fetcher` implementation that extracts dependencies + from the current project's `mix.exs` file. + + This module is responsible for reading and normalizing direct dependencies + defined in the project configuration, returning them in a standard format + expected by the submission tool. """ @behaviour MixDependencySubmission.Fetcher alias MixDependencySubmission.Fetcher + @doc """ + Fetches and normalizes the direct dependencies defined in the `mix.exs` file. + + This implementation reads the project configuration via + `Mix.Project.config()[:deps]` and normalizes each dependency entry. + + ## Examples + + iex> %{ + ...> burrito: %{ + ...> scm: Hex.SCM, + ...> mix_dep: _dep, + ...> relationship: :direct, + ...> scope: :runtime + ...> } + ...> } = + ...> MixDependencySubmission.Fetcher.MixFile.fetch() + + Note: This test assumes an Elixir project that is currently loaded with a + `mix.exs` file in place. + """ @impl Fetcher def fetch do Mix.Project.config()[:deps] |> List.wrap() |> Map.new(&normalize_dep/1) diff --git a/lib/mix_dependency_submission/fetcher/mix_lock.ex b/lib/mix_dependency_submission/fetcher/mix_lock.ex index cc3cd08..842558d 100644 --- a/lib/mix_dependency_submission/fetcher/mix_lock.ex +++ b/lib/mix_dependency_submission/fetcher/mix_lock.ex @@ -1,6 +1,9 @@ defmodule MixDependencySubmission.Fetcher.MixLock do @moduledoc """ - Fetch Dependencies from mix.lock directly. + Fetches dependencies from the `mix.lock` file. + + This module implements the `MixDependencySubmission.Fetcher` behaviour to extract + locked dependencies and convert them into the expected internal format. """ @behaviour MixDependencySubmission.Fetcher @@ -9,6 +12,19 @@ defmodule MixDependencySubmission.Fetcher.MixLock do require Logger + @doc """ + Reads and normalizes locked dependencies from the `mix.lock` file. + + Returns `nil` if the lockfile doesn't exist. + + ## Examples + + iex> %{burrito: %{scm: Hex.SCM, mix_lock: [:hex, :burrito | _]}} = + ...> MixDependencySubmission.Fetcher.MixLock.fetch() + + Note: This test assumes an Elixir project that is currently loaded with a + `mix.lock` file in place. + """ @impl Fetcher def fetch do lockfile_name = Mix.Project.config()[:lockfile] @@ -32,11 +48,13 @@ defmodule MixDependencySubmission.Fetcher.MixLock do else {:error, reason} -> Logger.warning("Failed to read lockfile #{lockfile}, reason: #{inspect(reason, pretty: true)}") + %{} end end - @spec normalize_dep(dep :: {Fetcher.app_name(), tuple()}) :: {Fetcher.app_name(), Fetcher.dependency()} + @spec normalize_dep(dep :: {Fetcher.app_name(), tuple()}) :: + {Fetcher.app_name(), Fetcher.dependency()} defp normalize_dep({app, lock} = _dep) do scm = Enum.find(Mix.SCM.available(), & &1.format_lock(lock: lock)) diff --git a/lib/mix_dependency_submission/fetcher/mix_runtime.ex b/lib/mix_dependency_submission/fetcher/mix_runtime.ex index d8b77b2..3b75251 100644 --- a/lib/mix_dependency_submission/fetcher/mix_runtime.ex +++ b/lib/mix_dependency_submission/fetcher/mix_runtime.ex @@ -1,12 +1,38 @@ defmodule MixDependencySubmission.Fetcher.MixRuntime do @moduledoc """ - Fetch Dependencies from Mix Runtime. + Fetches dependencies from the compiled Mix project at runtime. + + This fetcher uses `Mix.Project.deps_tree/1` and related project metadata to + collect runtime dependency information including versions, SCMs, and + relationships. """ @behaviour MixDependencySubmission.Fetcher alias MixDependencySubmission.Fetcher + @doc """ + Fetches all runtime dependencies from the current Mix project. + + Includes both direct and indirect dependencies as resolved from the dependency + tree at runtime. + + ## Examples + + iex> %{ + ...> burrito: %{ + ...> scm: Hex.SCM, + ...> dependencies: [:jason, :req, :typed_struct], + ...> mix_config: _config, + ...> relationship: :direct, + ...> scope: :runtime, + ...> version: _version + ...> } + ...> } = + ...> MixDependencySubmission.Fetcher.MixRuntime.fetch() + + Note: This test assumes an Elixir project that is currently loaded. + """ @impl Fetcher def fetch do root_deps = [depth: 1] |> Mix.Project.deps_tree() |> Map.keys() @@ -28,7 +54,9 @@ defmodule MixDependencySubmission.Fetcher.MixRuntime do config = if Elixir.File.exists?(dep_path) do - Mix.Project.in_project(app, Map.fetch!(deps_paths, app), fn _module -> Mix.Project.config() end) + Mix.Project.in_project(app, Map.fetch!(deps_paths, app), fn _module -> + Mix.Project.config() + end) else [] end diff --git a/lib/mix_dependency_submission/scm.ex b/lib/mix_dependency_submission/scm.ex index a1d1734..a34caa6 100644 --- a/lib/mix_dependency_submission/scm.ex +++ b/lib/mix_dependency_submission/scm.ex @@ -1,11 +1,20 @@ defmodule MixDependencySubmission.SCM do - @moduledoc false + @moduledoc """ + Defines the behavior and helper types for working with source control managers + (SCMs) in the Mix dependency submission context. + + SCM implementations are responsible for generating package URLs (`purl`) and + extracting dependency metadata from `mix.lock` or `mix.exs`. + + Implementations must live under `MixDependencySubmission.SCM.[NAME]` and match + the name of the corresponding module in `Mix.SCM.available/0`. + """ @typedoc """ Lock for the dependency as a list. - **Always only match the start of the list. The list can be extended to include - further contents in the future at the end of the list.** + **Always only match the start of the list. The list can be extended in the + future.** ## Examples @@ -30,29 +39,45 @@ defmodule MixDependencySubmission.SCM do @type dep() :: {app_name(), requirement :: String.t() | nil, opts :: Keyword.t()} @doc """ - Create a package url for the given `dep`. + Creates a package URL (`purl`) from a declared dependency. + Implementations are expected to convert the given Mix dependency (from `mix.exs`) + into a `Purl` struct. """ @callback mix_dep_to_purl(dep(), version :: String.t() | nil) :: Purl.t() @doc """ - Create a package url for the given `dep`. - - ## Examples - - iex> MixDependencySubmission.SCM.Hex.SCM.mix_dependency_to_package_url(:credo) - {:ok, Purl.parse!("pkg:hex/credo@1.7.0")} - - iex> MixDependencySubmission.SCM.Hex.SCM.mix_dependency_to_package_url(:invalid) - :error + Creates a package URL (`purl`) from a locked dependency. + This is used when data is available from `mix.lock`. """ @callback mix_lock_to_purl(app :: atom(), lock :: lock()) :: Purl.t() + @doc """ + Returns a list of app names representing sub-dependencies found in the lock. + + Only used if the SCM implementation supports this and provides custom logic. + """ @callback mix_lock_deps(lock :: lock()) :: [app_name()] @optional_callbacks mix_lock_deps: 1, mix_lock_to_purl: 2 + @doc """ + Returns the module implementing SCM-specific behavior for a given SCM module. + + Looks for a corresponding module under `MixDependencySubmission.SCM.*`. + + Returns `nil` if no implementation exists or the module is not loaded. + + ## Examples + + iex> MixDependencySubmission.SCM.implementation(Mix.SCM.Path) + MixDependencySubmission.SCM.Mix.SCM.Path + + iex> MixDependencySubmission.SCM.implementation(Unknown.SCM) + nil + + """ @spec implementation(module()) :: module() | nil def implementation(scm) when is_atom(scm) do if scm in Mix.SCM.available() do diff --git a/lib/mix_dependency_submission/scm/hex/scm.ex b/lib/mix_dependency_submission/scm/hex/scm.ex index 79d5ec6..e668da7 100644 --- a/lib/mix_dependency_submission/scm/hex/scm.ex +++ b/lib/mix_dependency_submission/scm/hex/scm.ex @@ -1,10 +1,34 @@ defmodule MixDependencySubmission.SCM.Hex.SCM do - @moduledoc false + @moduledoc """ + SCM implementation for Hex packages. + + Handles the conversion of Hex dependencies into `purl` format and extraction + of dependency information from `mix.exs` and `mix.lock`. + """ @behaviour MixDependencySubmission.SCM alias MixDependencySubmission.SCM + @doc """ + Creates a package URL (`purl`) from a declared Mix dependency (`mix.exs`). + + Attempts to resolve the repository URL and include it as a `qualifier`. + + ## Examples + + iex> MixDependencySubmission.SCM.Hex.SCM.mix_dep_to_purl( + ...> {:jason, "~> 1.0", [hex: :jason, repo: "hexpm"]}, + ...> "1.4.0" + ...> ) + %Purl{ + type: "hex", + namespace: [], + name: "jason", + version: "1.4.0", + qualifiers: %{} + } + """ @impl SCM def mix_dep_to_purl({_app, requirement, opts}, version) do qualifiers = @@ -22,9 +46,45 @@ defmodule MixDependencySubmission.SCM.Hex.SCM do }) end + @doc """ + Creates a package URL (`purl`) from a locked dependency (`mix.lock` entry). + + Attempts to include the repository URL as a `qualifier`, if resolvable. + + ## Examples + + iex> lock = [ + ...> :hex, + ...> :jason, + ...> "1.4.0", + ...> "checksum", + ...> [:mix], + ...> [], + ...> "hexpm", + ...> "checksum" + ...> ] + ...> + ...> MixDependencySubmission.SCM.Hex.SCM.mix_lock_to_purl(:jason, lock) + %Purl{ + type: "hex", + namespace: [], + name: "jason", + version: "1.4.0", + qualifiers: %{} + } + """ @impl SCM def mix_lock_to_purl(_app, lock) do - [:hex, package_name, version, _inner_checksum, _managers, _deps, repo, _outer_checksum | _rest] = lock + [ + :hex, + package_name, + version, + _inner_checksum, + _managers, + _deps, + repo, + _outer_checksum | _rest + ] = lock qualifiers = case repository_url(repo) do @@ -41,9 +101,40 @@ defmodule MixDependencySubmission.SCM.Hex.SCM do }) end + @doc """ + Returns the list of app names that are dependencies of the given locked dependency. + + ## Examples + + iex> lock = [ + ...> :hex, + ...> :my_app, + ...> "0.1.0", + ...> "checksum", + ...> [:mix], + ...> [ + ...> {:dep_a, "~> 0.1.0", [hex: :dep_a]}, + ...> {:dep_b, "~> 0.2.0", [hex: :dep_b]} + ...> ], + ...> "hexpm", + ...> "checksum" + ...> ] + ...> + ...> MixDependencySubmission.SCM.Hex.SCM.mix_lock_deps(lock) + [:dep_a, :dep_b] + """ @impl SCM def mix_lock_deps(lock) do - [:hex, _package_name, _version, _inner_checksum, _managers, deps, _repo, _outer_checksum | _rest] = lock + [ + :hex, + _package_name, + _version, + _inner_checksum, + _managers, + deps, + _repo, + _outer_checksum | _rest + ] = lock Enum.map(deps, fn {app, _requirement, _opts} -> app end) end diff --git a/lib/mix_dependency_submission/scm/mix/scm/git.ex b/lib/mix_dependency_submission/scm/mix/scm/git.ex index aad1e7c..97188fb 100644 --- a/lib/mix_dependency_submission/scm/mix/scm/git.ex +++ b/lib/mix_dependency_submission/scm/mix/scm/git.ex @@ -1,10 +1,36 @@ defmodule MixDependencySubmission.SCM.Mix.SCM.Git do - @moduledoc false + @moduledoc """ + SCM implementation for Git-based Mix dependencies. + + Handles conversion of Git dependencies declared in `mix.exs` or found in + `mix.lock` into package URLs (purl). Falls back to a generic purl format if + the repository URL does not represent a specific purl type like `pkg:github`. + """ @behaviour MixDependencySubmission.SCM alias MixDependencySubmission.SCM + @doc """ + Creates a package URL (`purl`) from a Git-based Mix dependency. + + Attempts to infer the version from `:ref`, `:branch`, or `:tag`, or falls back + to the requirement. + + ## Examples + + iex> MixDependencySubmission.SCM.Mix.SCM.Git.mix_dep_to_purl( + ...> {:my_app, nil, [git: "https://github.com/example/my_app.git", tag: "v1.0.0"]}, + ...> nil + ...> ) + %Purl{ + type: "github", + name: "my_app", + namespace: ["example"], + version: "v1.0.0" + } + + """ @impl SCM def mix_dep_to_purl({app, requirement, opts}, version) do version = version || opts[:ref] || opts[:branch] || opts[:tag] || requirement @@ -23,6 +49,25 @@ defmodule MixDependencySubmission.SCM.Mix.SCM.Git do end end + @doc """ + Creates a package URL (`purl`) from a Git-based `mix.lock` entry. + + Uses the repository URL and Git revision as the version. + + ## Examples + + iex> MixDependencySubmission.SCM.Mix.SCM.Git.mix_lock_to_purl( + ...> :my_app, + ...> [:git, "https://github.com/example/my_app.git", "abc123"] + ...> ) + %Purl{ + type: "github", + name: "my_app", + namespace: ["example"], + version: "abc123" + } + + """ @impl SCM def mix_lock_to_purl(app, lock) do [:git, repo_url, revision | _rest] = lock diff --git a/lib/mix_dependency_submission/scm/mix/scm/path.ex b/lib/mix_dependency_submission/scm/mix/scm/path.ex index 94d4e2e..9c4432b 100644 --- a/lib/mix_dependency_submission/scm/mix/scm/path.ex +++ b/lib/mix_dependency_submission/scm/mix/scm/path.ex @@ -1,24 +1,49 @@ defmodule MixDependencySubmission.SCM.Mix.SCM.Path do - @moduledoc false + @moduledoc """ + SCM implementation for path-based Mix dependencies. + + Generates a generic `purl` for dependencies declared with `:path` in + `mix.exs`. + """ @behaviour MixDependencySubmission.SCM alias MixDependencySubmission.SCM + @doc """ + Creates a generic package URL (`purl`) from a path-based Mix dependency. + + Uses the `requirement` from `mix.exs`, or falls back to the version if available. + + ## Examples + + iex> MixDependencySubmission.SCM.Mix.SCM.Path.mix_dep_to_purl( + ...> {:my_dep, "~> 0.1.0", path: "deps/my_dep"}, + ...> nil + ...> ) + %Purl{ + type: "generic", + name: "my_dep", + version: "~> 0.1.0" + } + + iex> MixDependencySubmission.SCM.Mix.SCM.Path.mix_dep_to_purl( + ...> {:my_dep, nil, path: "deps/my_dep"}, + ...> "0.1.0" + ...> ) + %Purl{ + type: "generic", + name: "my_dep", + version: "0.1.0" + } + + """ @impl SCM - def mix_dep_to_purl({app, requirement, opts}, version) do - version = version || opts[:ref] || opts[:branch] || opts[:tag] || requirement - - case Purl.from_resource_uri(opts[:git]) do - {:ok, purl} -> - %{purl | version: version} - - :error -> - Purl.new!(%Purl{ - type: "generic", - name: Atom.to_string(app), - version: version - }) - end + def mix_dep_to_purl({app, requirement, _opts}, version) do + Purl.new!(%Purl{ + type: "generic", + name: Atom.to_string(app), + version: requirement || version + }) end end diff --git a/lib/mix_dependency_submission/submission.ex b/lib/mix_dependency_submission/submission.ex index 100244e..fbf4f99 100644 --- a/lib/mix_dependency_submission/submission.ex +++ b/lib/mix_dependency_submission/submission.ex @@ -1,5 +1,9 @@ defmodule MixDependencySubmission.Submission do - @moduledoc false + @moduledoc """ + Represents the top-level dependency submission payload. + + See https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28#create-a-snapshot-of-dependencies-for-a-repository + """ alias MixDependencySubmission.Submission.Detector alias MixDependencySubmission.Submission.Job @@ -31,17 +35,62 @@ defmodule MixDependencySubmission.Submission do url: URI.new!(@url) } + @doc """ + Creates a new dependency submission struct from GitHub-related metadata and resolved manifests. + + ## Examples + + iex> MixDependencySubmission.Submission.new(%{ + ...> github_job_id: "job123", + ...> github_workflow: "build.yml", + ...> ref: "refs/heads/main", + ...> sha: "sha", + ...> manifests: %{}, + ...> scanned: ~U[2025-04-19 10:05:38.170646Z] + ...> }) + %MixDependencySubmission.Submission{ + version: 0, + job: %MixDependencySubmission.Submission.Job{ + id: "job123", + correlator: "build.yml job123", + html_url: nil + }, + sha: "sha", + ref: "refs/heads/main", + detector: %MixDependencySubmission.Submission.Detector{ + name: "mix_dependency_submission", + version: #{inspect(Version.parse!(@version))}, + url: %URI{ + scheme: "https", + userinfo: nil, + host: "github.com", + port: 443, + path: "/erlef/mix-dependency-submission", + query: nil, + fragment: nil + } + }, + scanned: ~U[2025-04-19 10:05:38.170646Z], + metadata: %{}, + manifests: %{} + } + + """ @spec new( options :: %{ - github_job_id: String.t(), - github_workflow: String.t(), - ref: String.t(), - sha: String.t(), - manifests: manifests() + required(:github_job_id) => String.t(), + required(:github_workflow) => String.t(), + required(:ref) => String.t(), + required(:sha) => String.t(), + required(:manifests) => manifests(), + optional(:scanned) => DateTime.t() } ) :: t() - def new(%{github_job_id: github_job_id, github_workflow: github_workflow, sha: sha, ref: ref, manifests: manifests}) do + def new( + %{github_job_id: github_job_id, github_workflow: github_workflow, sha: sha, ref: ref, manifests: manifests} = + options + ) do %__MODULE__{ version: 0, job: %Job{ @@ -52,7 +101,7 @@ defmodule MixDependencySubmission.Submission do ref: ref, detector: @detector, metadata: %{}, - scanned: DateTime.utc_now(), + scanned: options[:scanned] || DateTime.utc_now(), manifests: manifests } end diff --git a/lib/mix_dependency_submission/submission/detector.ex b/lib/mix_dependency_submission/submission/detector.ex index 8b400ed..fc1e747 100644 --- a/lib/mix_dependency_submission/submission/detector.ex +++ b/lib/mix_dependency_submission/submission/detector.ex @@ -1,5 +1,9 @@ defmodule MixDependencySubmission.Submission.Detector do - @moduledoc false + @moduledoc """ + Represents the detector entry in the submission manifest. + + See https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28#create-a-snapshot-of-dependencies-for-a-repository + """ @type t :: %__MODULE__{ name: String.t(), diff --git a/lib/mix_dependency_submission/submission/job.ex b/lib/mix_dependency_submission/submission/job.ex index caa111f..fcd8df7 100644 --- a/lib/mix_dependency_submission/submission/job.ex +++ b/lib/mix_dependency_submission/submission/job.ex @@ -1,5 +1,9 @@ defmodule MixDependencySubmission.Submission.Job do - @moduledoc false + @moduledoc """ + Represents the job entry in the submission manifest. + + See https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28#create-a-snapshot-of-dependencies-for-a-repository + """ @type t :: %__MODULE__{ id: String.t(), diff --git a/lib/mix_dependency_submission/submission/manifest.ex b/lib/mix_dependency_submission/submission/manifest.ex index 388b2dd..709df64 100644 --- a/lib/mix_dependency_submission/submission/manifest.ex +++ b/lib/mix_dependency_submission/submission/manifest.ex @@ -1,5 +1,9 @@ defmodule MixDependencySubmission.Submission.Manifest do - @moduledoc false + @moduledoc """ + Represents a manifest entry in the submission payload. + + See https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28#create-a-snapshot-of-dependencies-for-a-repository + """ alias MixDependencySubmission.Submission.Manifest.Dependency alias MixDependencySubmission.Submission.Manifest.File diff --git a/lib/mix_dependency_submission/submission/manifest/dependency.ex b/lib/mix_dependency_submission/submission/manifest/dependency.ex index c64d95b..cadbc05 100644 --- a/lib/mix_dependency_submission/submission/manifest/dependency.ex +++ b/lib/mix_dependency_submission/submission/manifest/dependency.ex @@ -1,5 +1,12 @@ defmodule MixDependencySubmission.Submission.Manifest.Dependency do - @moduledoc false + @moduledoc """ + Represents a dependency entry in the submission manifest. + + Used to describe individual dependencies with metadata, scope, and + relationship information, including any nested dependencies via `purl`s. + + See https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28#create-a-snapshot-of-dependencies-for-a-repository + """ @type relationship() :: :direct | :indirect @type scope() :: :runtime | :development diff --git a/lib/mix_dependency_submission/submission/manifest/file.ex b/lib/mix_dependency_submission/submission/manifest/file.ex index af9652e..781392f 100644 --- a/lib/mix_dependency_submission/submission/manifest/file.ex +++ b/lib/mix_dependency_submission/submission/manifest/file.ex @@ -1,5 +1,9 @@ defmodule MixDependencySubmission.Submission.Manifest.File do - @moduledoc false + @moduledoc """ + Represents a file entry in the submission manifest. + + See https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28#create-a-snapshot-of-dependencies-for-a-repository + """ @type t :: %__MODULE__{ source_location: Path.t() | nil diff --git a/lib/mix_dependency_submission/util.ex b/lib/mix_dependency_submission/util.ex index 30dde20..81d4b13 100644 --- a/lib/mix_dependency_submission/util.ex +++ b/lib/mix_dependency_submission/util.ex @@ -1,7 +1,8 @@ defmodule MixDependencySubmission.Util do @moduledoc false - @spec in_project(path :: Path.t(), fun :: (module() | nil -> result)) :: result when result: term() + @spec in_project(path :: Path.t(), fun :: (module() | nil -> result)) :: result + when result: term() def in_project(path, fun) do setup_context(fn -> path diff --git a/mix.exs b/mix.exs index 1eb7abd..d05e84c 100644 --- a/mix.exs +++ b/mix.exs @@ -61,17 +61,20 @@ defmodule MixDependencySubmission.MixProject do source_url: @source_url, source_ref: "v" <> @version, main: "readme", - extras: ["README.md"] + extras: ["README.md"], + nest_modules_by_prefix: [MixDependencySubmission] ] end defp deps do + # styler:sort [ {:burrito, "~> 1.0"}, {:credo, "~> 1.0", only: [:dev], runtime: false}, {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, - {:excoveralls, "~> 0.5", only: [:test], runtime: false}, + {:doctest_formatter, "~> 0.3.1", runtime: false}, {:ex_doc, ">= 0.0.0", only: [:dev], runtime: false}, + {:excoveralls, "~> 0.5", only: [:test], runtime: false}, {:hex, github: "hexpm/hex", runtime: false}, {:jason, "~> 1.4"}, {:optimus, "~> 0.2"}, diff --git a/mix.lock b/mix.lock index 0e82886..f8b884c 100644 --- a/mix.lock +++ b/mix.lock @@ -3,6 +3,7 @@ "burrito": {:hex, :burrito, "1.3.0", "4be8504185250756ff4a8770d0c0d91dbfe518d2faa5f1888f13b00540028c59", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.5.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "a53f6bc0644bfd998164d68714c9af04291c220f5f7d0c90cb9616780cc60165"}, "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, + "doctest_formatter": {:hex, :doctest_formatter, "0.3.1", "a3fd87c1f75e8a78e7737ec4a4494800ddda705998a59320b87fe4c59c030794", [:mix], [], "hexpm", "3c092540d8b73ffc526a92daa2dc2ecd50714f14325eeacbc7b4e790f890443a"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, diff --git a/test/mix_dependency_submission/api_client_test.exs b/test/mix_dependency_submission/api_client_test.exs index d2118c6..17c39c5 100644 --- a/test/mix_dependency_submission/api_client_test.exs +++ b/test/mix_dependency_submission/api_client_test.exs @@ -4,6 +4,8 @@ defmodule MixDependencySubmission.ApiClientTest do alias MixDependencySubmission.ApiClient alias MixDependencySubmission.Submission + doctest ApiClient, except: [submit: 4] + describe inspect(&ApiClient.submit/4) do test "sends correct request" do Req.Test.stub(ApiClient, fn conn -> diff --git a/test/mix_dependency_submission/cli_test.exs b/test/mix_dependency_submission/cli_test.exs new file mode 100644 index 0000000..aaa5e6e --- /dev/null +++ b/test/mix_dependency_submission/cli_test.exs @@ -0,0 +1,7 @@ +defmodule MixDependencySubmission.CLITest do + use ExUnit.Case, async: true + + alias MixDependencySubmission.CLI + + doctest CLI +end diff --git a/test/mix_dependency_submission/fetcher/mix_file_test.exs b/test/mix_dependency_submission/fetcher/mix_file_test.exs index 0f69e12..46e9a87 100644 --- a/test/mix_dependency_submission/fetcher/mix_file_test.exs +++ b/test/mix_dependency_submission/fetcher/mix_file_test.exs @@ -4,7 +4,7 @@ defmodule MixDependencySubmission.Fetcher.MixFileTest do alias MixDependencySubmission.Fetcher.MixFile alias MixDependencySubmission.Util - doctest MixDependencySubmission + doctest MixFile describe inspect(&MixFile.fetch/1) do @tag :tmp_dir diff --git a/test/mix_dependency_submission/fetcher/mix_lock_test.exs b/test/mix_dependency_submission/fetcher/mix_lock_test.exs index 1706d0c..ebccfaf 100644 --- a/test/mix_dependency_submission/fetcher/mix_lock_test.exs +++ b/test/mix_dependency_submission/fetcher/mix_lock_test.exs @@ -4,7 +4,7 @@ defmodule MixDependencySubmission.Fetcher.MixLockTest do alias MixDependencySubmission.Fetcher.MixLock alias MixDependencySubmission.Util - doctest MixDependencySubmission + doctest MixLock describe inspect(&MixLock.fetch/1) do @tag :tmp_dir diff --git a/test/mix_dependency_submission/fetcher/mix_runtime_test.exs b/test/mix_dependency_submission/fetcher/mix_runtime_test.exs index 601f900..bcf7ac6 100644 --- a/test/mix_dependency_submission/fetcher/mix_runtime_test.exs +++ b/test/mix_dependency_submission/fetcher/mix_runtime_test.exs @@ -4,7 +4,7 @@ defmodule MixDependencySubmission.Fetcher.MixRuntimeTest do alias MixDependencySubmission.Fetcher.MixRuntime alias MixDependencySubmission.Util - doctest MixDependencySubmission + doctest MixRuntime describe inspect(&MixRuntime.fetch/1) do @tag :tmp_dir diff --git a/test/mix_dependency_submission/fetcher_test.exs b/test/mix_dependency_submission/fetcher_test.exs new file mode 100644 index 0000000..1fe4d4e --- /dev/null +++ b/test/mix_dependency_submission/fetcher_test.exs @@ -0,0 +1,7 @@ +defmodule MixDependencySubmission.FetcherTest do + use MixDependencySubmission.FixtureCase, async: false + + alias MixDependencySubmission.Fetcher + + doctest Fetcher +end diff --git a/test/mix_dependency_submission/scm/hex/scm_test.exs b/test/mix_dependency_submission/scm/hex/scm_test.exs new file mode 100644 index 0000000..7d977a5 --- /dev/null +++ b/test/mix_dependency_submission/scm/hex/scm_test.exs @@ -0,0 +1,7 @@ +defmodule MixDependencySubmission.SCM.Hex.SCMTest do + use MixDependencySubmission.FixtureCase, async: false + + alias MixDependencySubmission.SCM.Hex.SCM + + doctest SCM +end diff --git a/test/mix_dependency_submission/scm/mix/scm/git_test.exs b/test/mix_dependency_submission/scm/mix/scm/git_test.exs new file mode 100644 index 0000000..2b076fc --- /dev/null +++ b/test/mix_dependency_submission/scm/mix/scm/git_test.exs @@ -0,0 +1,7 @@ +defmodule MixDependencySubmission.SCM.Mix.SCM.GitTest do + use MixDependencySubmission.FixtureCase, async: false + + alias MixDependencySubmission.SCM.Mix.SCM.Git + + doctest Git +end diff --git a/test/mix_dependency_submission/scm/mix/scm/path_test.exs b/test/mix_dependency_submission/scm/mix/scm/path_test.exs new file mode 100644 index 0000000..3feab79 --- /dev/null +++ b/test/mix_dependency_submission/scm/mix/scm/path_test.exs @@ -0,0 +1,7 @@ +defmodule MixDependencySubmission.SCM.Mix.SCM.PathTest do + use MixDependencySubmission.FixtureCase, async: false + + alias MixDependencySubmission.SCM.Mix.SCM.Path + + doctest Path +end diff --git a/test/mix_dependency_submission/scm_test.exs b/test/mix_dependency_submission/scm_test.exs new file mode 100644 index 0000000..a1f87ef --- /dev/null +++ b/test/mix_dependency_submission/scm_test.exs @@ -0,0 +1,7 @@ +defmodule MixDependencySubmission.SCMTest do + use MixDependencySubmission.FixtureCase, async: false + + alias MixDependencySubmission.SCM + + doctest SCM +end diff --git a/test/mix_dependency_submission/submission/detector_test.exs b/test/mix_dependency_submission/submission/detector_test.exs index 16c98b8..3fb7f13 100644 --- a/test/mix_dependency_submission/submission/detector_test.exs +++ b/test/mix_dependency_submission/submission/detector_test.exs @@ -3,6 +3,8 @@ defmodule MixDependencySubmission.Submission.DetectorTest do alias MixDependencySubmission.Submission.Detector + doctest Detector + describe "Jason.Encoder" do test "encodes filled struct" do detector = %Detector{ diff --git a/test/mix_dependency_submission/submission/job_test.exs b/test/mix_dependency_submission/submission/job_test.exs index 4fe1f2e..49df9c7 100644 --- a/test/mix_dependency_submission/submission/job_test.exs +++ b/test/mix_dependency_submission/submission/job_test.exs @@ -3,6 +3,8 @@ defmodule MixDependencySubmission.Submission.JobTest do alias MixDependencySubmission.Submission.Job + doctest Job + describe "Jason.Encoder" do test "encodes filled struct" do job = %Job{ diff --git a/test/mix_dependency_submission/submission/manifest/dependency_test.exs b/test/mix_dependency_submission/submission/manifest/dependency_test.exs index 33f89c6..e834196 100644 --- a/test/mix_dependency_submission/submission/manifest/dependency_test.exs +++ b/test/mix_dependency_submission/submission/manifest/dependency_test.exs @@ -3,6 +3,8 @@ defmodule MixDependencySubmission.Submission.Manifest.DependencyTest do alias MixDependencySubmission.Submission.Manifest.Dependency + doctest Dependency + describe "Jason.Encoder" do test "encodes filled struct" do dependency = %Dependency{ diff --git a/test/mix_dependency_submission/submission/manifest/file_test.exs b/test/mix_dependency_submission/submission/manifest/file_test.exs index e48f161..e65348f 100644 --- a/test/mix_dependency_submission/submission/manifest/file_test.exs +++ b/test/mix_dependency_submission/submission/manifest/file_test.exs @@ -3,6 +3,8 @@ defmodule MixDependencySubmission.Submission.Manifest.FileTest do alias MixDependencySubmission.Submission.Manifest.File + doctest File + describe "Jason.Encoder" do test "encodes filled struct" do file = %File{ diff --git a/test/mix_dependency_submission/submission/manifest_test.exs b/test/mix_dependency_submission/submission/manifest_test.exs index 03837e7..8e091fe 100644 --- a/test/mix_dependency_submission/submission/manifest_test.exs +++ b/test/mix_dependency_submission/submission/manifest_test.exs @@ -4,6 +4,8 @@ defmodule MixDependencySubmission.Submission.ManifestTest do alias MixDependencySubmission.Submission.Manifest alias MixDependencySubmission.Submission.Manifest.File + doctest Manifest + describe "Jason.Encoder" do test "encodes filled struct" do manifest = %Manifest{ diff --git a/test/mix_dependency_submission/submission_test.exs b/test/mix_dependency_submission/submission_test.exs new file mode 100644 index 0000000..fe6ad23 --- /dev/null +++ b/test/mix_dependency_submission/submission_test.exs @@ -0,0 +1,46 @@ +defmodule MixDependencySubmission.SubmissionTest do + use ExUnit.Case, async: true + + alias MixDependencySubmission.Submission + + doctest Submission + + describe "Jason.Encoder" do + test "encodes filled struct" do + datetime = DateTime.utc_now() + + submission = %Submission{ + version: 0, + job: %Submission.Job{ + id: "test", + correlator: "test", + html_url: URI.parse("http://example.com") + }, + sha: "sha", + ref: "ref", + detector: %Submission.Detector{ + name: "test", + version: Version.parse!("1.0.0"), + url: URI.parse("http://example.com") + }, + metadata: %{"foo" => "bar"}, + scanned: datetime, + manifests: %{} + } + + datetime_string = DateTime.to_iso8601(datetime) + + assert %{ + "detector" => %{"name" => "test", "url" => "http://example.com", "version" => "1.0.0"}, + "job" => %{"correlator" => "test", "html_url" => "http://example.com", "id" => "test"}, + "manifests" => %{}, + "metadata" => %{"foo" => "bar"}, + "ref" => "ref", + "scanned" => ^datetime_string, + "sha" => "sha", + "version" => 0 + } = + submission |> Jason.encode!() |> Jason.decode!() + end + end +end diff --git a/test/mix_dependency_submission_test.exs b/test/mix_dependency_submission_test.exs index 71bc284..26e1f8e 100644 --- a/test/mix_dependency_submission_test.exs +++ b/test/mix_dependency_submission_test.exs @@ -7,7 +7,7 @@ defmodule MixDependencySubmissionTest do alias MixDependencySubmission.Submission.Manifest.Dependency alias MixDependencySubmission.Util - doctest MixDependencySubmission + doctest MixDependencySubmission, except: [submission: 1] describe inspect(&MixDependencySubmission.submission/1) do @tag :tmp_dir