From e229b45b3561f0ddf62f7d3989d6115293066e57 Mon Sep 17 00:00:00 2001 From: AJ Foster Date: Mon, 13 Dec 2021 22:29:06 -0500 Subject: [PATCH] Add hex.registry add command --- lib/mix/tasks/hex.registry.ex | 225 ++++++++++ src/mix_hex_registry.erl | 14 + test/mix/tasks/hex.registry_test.exs | 600 ++++++++++++++++++--------- 3 files changed, 647 insertions(+), 192 deletions(-) diff --git a/lib/mix/tasks/hex.registry.ex b/lib/mix/tasks/hex.registry.ex index ce61ab56..26e926ff 100644 --- a/lib/mix/tasks/hex.registry.ex +++ b/lib/mix/tasks/hex.registry.ex @@ -58,6 +58,26 @@ defmodule Mix.Tasks.Hex.Registry do * `--name` - The name of the registry * `--private-key` - Path to the private key + + ## Add a package + + $ mix hex.registry add PUBLIC_DIR PACKAGE1 PACKAGE2 ... + + To add one or more packages to an existing registry, supply the public directory of the registry + and paths to the new packages. This action also requires the private key used to generate the + original registry: + + $ mix hex.registry add public --private-key=private_key.pem foo-1.0.0.tar + * reading public/name + * reading public/versions + * moving foo-1.0.0.tar -> public/tarballs/foo-1.0.0.tar + * reading public/packages/foo + * updating public/packages/foo + * updating public/names + * updating public/versions + + Supplying a `--name` is optional. If given, an error will be raised if the existing registry's + name is different than the supplied value. """ @impl true def run(args) do @@ -65,6 +85,9 @@ defmodule Mix.Tasks.Hex.Registry do {opts, args} = Hex.OptionParser.parse!(args, strict: @switches) case args do + ["add", public_dir, package | additional_packages] -> + add(public_dir, [package | additional_packages], opts) + ["build", public_dir] -> build(public_dir, opts) @@ -72,6 +95,7 @@ defmodule Mix.Tasks.Hex.Registry do Mix.raise(""" Invalid arguments, expected one of: + mix hex.registry add PUBLIC_DIR PACKAGE mix hex.registry build PUBLIC_DIR """) end @@ -80,10 +104,102 @@ defmodule Mix.Tasks.Hex.Registry do @impl true def tasks() do [ + {"add PUBLIC_DIR PACKAGE", "Add package to a local registry"}, {"build PUBLIC_DIR", "Build a local registry"} ] end + ## Add + + defp add(public_dir, packages, opts) do + repo_name_or_nil = opts[:name] + private_key_path = opts[:private_key] || raise "missing --private-key" + private_key = private_key_path |> File.read!() |> decode_private_key() + add(repo_name_or_nil, public_dir, private_key, packages) + end + + defp add(repo_name_or_nil, public_dir, private_key, packages) do + public_key = ensure_public_key(private_key, public_dir) + + existing_names = + read_names!(repo_name_or_nil, public_dir, public_key) + |> Enum.map(fn %{name: name, updated_at: updated_at} -> {name, updated_at} end) + |> Enum.into(%{}) + + existing_versions = + read_versions!(repo_name_or_nil, public_dir, public_key) + |> Enum.map(fn %{name: name, versions: versions} -> + {name, %{updated_at: existing_names[name], versions: versions}} + end) + |> Enum.into(%{}) + + tarball_dir = Path.join(public_dir, "tarballs") + create_directory(tarball_dir) + repo_name = repo_name_or_nil || read_repository_name!(public_dir, private_key) + + paths_per_name = + packages + |> Enum.map(fn path -> move_file!(path, tarball_dir) end) + |> Enum.group_by(fn path -> + [name | _rest] = String.split(Path.basename(path), ["-", ".tar"], trim: true) + name + end) + + versions = + Enum.map(paths_per_name, fn {name, paths} -> + existing_releases = read_package(repo_name, public_dir, public_key, name) + + releases = + paths + |> Enum.map(&build_release(repo_name, &1)) + |> Enum.concat(existing_releases) + |> Enum.sort(&(Hex.Version.compare(&1.version, &2.version) == :lt)) + |> Enum.uniq_by(& &1.version) + + updated_at = + paths + |> Enum.map(&File.stat!(&1).mtime) + |> Enum.sort() + |> Enum.at(-1) + + previous_updated_at = get_in(existing_names, [name, :updated_at, :seconds]) + updated_at = %{seconds: max_updated_at(previous_updated_at, updated_at), nanos: 0} + + package = + :mix_hex_registry.build_package( + %{repository: repo_name, name: name, releases: releases}, + private_key + ) + + write_file("#{public_dir}/packages/#{name}", package) + versions = Enum.map(releases, & &1.version) + {name, %{updated_at: updated_at, versions: versions}} + end) + |> Enum.into(%{}) + + versions = Map.merge(existing_versions, versions) + + names = + for {name, %{updated_at: updated_at}} <- versions do + %{name: name, updated_at: updated_at} + end + + payload = %{repository: repo_name, packages: names} + names = :mix_hex_registry.build_names(payload, private_key) + write_file("#{public_dir}/names", names) + + versions = + for {name, %{versions: versions}} <- versions do + %{name: name, versions: versions} + end + + payload = %{repository: repo_name, packages: versions} + versions = :mix_hex_registry.build_versions(payload, private_key) + write_file("#{public_dir}/versions", versions) + end + + ## Build + defp build(public_dir, opts) do repo_name = opts[:name] || raise "missing --name" private_key_path = opts[:private_key] || raise "missing --private-key" @@ -151,6 +267,8 @@ defmodule Mix.Tasks.Hex.Registry do write_file("#{public_dir}/versions", versions) end + ## Registry utilities + @unix_epoch :calendar.datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}) @doc false @@ -158,6 +276,13 @@ defmodule Mix.Tasks.Hex.Registry do :calendar.datetime_to_gregorian_seconds(erl_datetime) - @unix_epoch end + defp max_updated_at(previous_as_unix_or_nil, nil), do: previous_as_unix_or_nil + defp max_updated_at(nil, current_as_datetime), do: to_unix(current_as_datetime) + + defp max_updated_at(previous_as_unix, current_as_datetime) do + max(previous_as_unix, to_unix(current_as_datetime)) + end + defp build_release(repo_name, tarball_path) do tarball = File.read!(tarball_path) {:ok, result} = :mix_hex_tarball.unpack(tarball, :memory) @@ -206,8 +331,80 @@ defmodule Mix.Tasks.Hex.Registry do {:error, :enoent} -> write_file(path, encoded_public_key) end + + encoded_public_key + end + + defp read_names!(repo_name, public_dir, public_key) do + path = Path.join(public_dir, "names") + payload = read_file!(path) + repo_name_or_no_verify = repo_name || :no_verify + + case :mix_hex_registry.unpack_names(payload, repo_name_or_no_verify, public_key) do + {:ok, names} -> + names + + _ -> + Mix.raise(""" + Invalid package name manifest at #{path} + + Is the repository name #{repo_name} correct? + """) + end + end + + defp read_versions!(repo_name, public_dir, public_key) do + path = Path.join(public_dir, "versions") + payload = read_file!(path) + repo_name_or_no_verify = repo_name || :no_verify + + case :mix_hex_registry.unpack_versions(payload, repo_name_or_no_verify, public_key) do + {:ok, versions} -> + versions + + _ -> + Mix.raise(""" + Invalid package version manifest at #{path} + + Is the repository name #{repo_name} correct? + """) + end end + defp read_repository_name!(public_dir, public_key) do + path = Path.join(public_dir, "names") + payload = read_file!(path) + + case :mix_hex_registry.get_repository_name(payload, public_key) do + {:ok, repo_name} -> + repo_name + + _ -> + Mix.raise(""" + Invalid package name manifest at #{path} + + Is the public key correct? + """) + end + end + + defp read_package(repo_name, public_dir, public_key, package_name) do + path = Path.join([public_dir, "packages", package_name]) + + case read_file(path) do + {:ok, payload} -> + case :mix_hex_registry.unpack_package(payload, repo_name, package_name, public_key) do + {:ok, package} -> package + _ -> [] + end + + _ -> + [] + end + end + + ## File utilities + defp create_directory(path) do unless File.dir?(path) do Hex.Shell.info(["* creating ", path]) @@ -215,6 +412,26 @@ defmodule Mix.Tasks.Hex.Registry do end end + defp read_file!(path) do + if File.exists?(path) do + Hex.Shell.info(["* reading ", path]) + else + Mix.raise("Error reading file #{path}") + end + + File.read!(path) + end + + defp read_file(path) do + if File.exists?(path) do + Hex.Shell.info(["* reading ", path]) + else + Hex.Shell.info(["* skipping ", path]) + end + + File.read(path) + end + defp write_file(path, data) do if File.exists?(path) do Hex.Shell.info(["* updating ", path]) @@ -226,6 +443,14 @@ defmodule Mix.Tasks.Hex.Registry do File.write!(path, data) end + defp move_file!(path, destination_dir) do + file = Path.basename(path) + destination_file = Path.join(destination_dir, file) + Hex.Shell.info(["* moving ", path, " -> ", destination_file]) + File.rename!(path, destination_file) + destination_file + end + defp remove_file(path) do Hex.Shell.info(["* removing ", path]) File.rm!(path) diff --git a/src/mix_hex_registry.erl b/src/mix_hex_registry.erl index 46f2aadc..c7ae8be2 100644 --- a/src/mix_hex_registry.erl +++ b/src/mix_hex_registry.erl @@ -8,6 +8,7 @@ decode_names/2, build_names/2, unpack_names/3, + get_repository_name/2, encode_versions/1, decode_versions/2, build_versions/2, @@ -62,6 +63,19 @@ decode_names(Payload, Repository) -> {error, unverified} end. +%% @doc +%% Get the encoded repository name. +get_repository_name(Payload, PublicKey) -> + case decode_and_verify_signed(zlib:gunzip(Payload), PublicKey) of + {ok, Names} -> decode_repository_name(Names); + Other -> Other + end. + +%% @private +decode_repository_name(Payload) -> + #{repository := Repository} = mix_hex_pb_names:decode_msg(Payload, 'Names'), + {ok, Repository}. + %% @doc %% Builds versions resource. build_versions(Versions, PrivateKey) -> diff --git a/test/mix/tasks/hex.registry_test.exs b/test/mix/tasks/hex.registry_test.exs index c2e1d61c..60eb61e0 100644 --- a/test/mix/tasks/hex.registry_test.exs +++ b/test/mix/tasks/hex.registry_test.exs @@ -1,201 +1,417 @@ defmodule Mix.Tasks.Hex.RegistryTest do use HexTest.Case - test "build" do - in_tmp(fn -> - bypass = setup_bypass() - - 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") - flush() + describe "add" do + test "adds a single package to an empty registry" do + in_tmp(fn -> + bypass = setup_bypass() - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - assert_received {:mix_shell, :info, ["* creating public/public_key"]} - assert_received {:mix_shell, :info, ["* creating public/tarballs"]} - assert_received {:mix_shell, :info, ["* creating public/names"]} - assert_received {:mix_shell, :info, ["* creating public/versions"]} - refute_received _ - - config = %{ - :mix_hex_core.default_config() - | repo_url: "http://localhost:#{bypass.port}", - repo_verify: false, - repo_verify_origin: false - } - - assert {:ok, {200, _, []}} = :mix_hex_repo.get_names(config) - assert {:ok, {200, _, []}} = :mix_hex_repo.get_versions(config) - - {:ok, %{tarball: tarball}} = :mix_hex_tarball.create(%{name: "foo", version: "0.10.0"}, []) - File.write!("public/tarballs/foo-0.10.0.tar", tarball) - - Mix.Task.reenable("hex.registry") - - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - assert_received {:mix_shell, :info, ["* creating public/packages/foo"]} - assert_received {:mix_shell, :info, ["* updating public/names"]} - assert_received {:mix_shell, :info, ["* updating public/versions"]} - refute_received _ - - assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) - assert [%{name: "foo", updated_at: %{seconds: updated_at}}] = names - - assert updated_at == - "public/tarballs/foo-0.10.0.tar" - |> File.stat!() - |> Map.fetch!(:mtime) - |> Mix.Tasks.Hex.Registry.to_unix() - - assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) - assert versions == [%{name: "foo", retired: [], versions: ["0.10.0"]}] - - {:ok, %{tarball: tarball}} = :mix_hex_tarball.create(%{name: "foo", version: "0.9.0"}, []) - File.write!("public/tarballs/foo-0.9.0.tar", tarball) - - Mix.Task.reenable("hex.registry") - - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} - assert_received {:mix_shell, :info, ["* updating public/names"]} - assert_received {:mix_shell, :info, ["* updating public/versions"]} - refute_received _ - - assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) - assert [%{name: "foo", updated_at: %{seconds: updated_at}}] = names - - assert updated_at == - "public/tarballs/foo-0.9.0.tar" - |> File.stat!() - |> Map.fetch!(:mtime) - |> Mix.Tasks.Hex.Registry.to_unix() - - assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) - assert versions == [%{name: "foo", retired: [], versions: ["0.9.0", "0.10.0"]}] - - # Versions with hyphen - {:ok, %{tarball: tarball}} = - :mix_hex_tarball.create(%{name: "foo", version: "1.0.0-rc"}, []) - - File.write!("public/tarballs/foo-1.0.0-rc.tar", tarball) - - Mix.Task.reenable("hex.registry") - - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} - assert_received {:mix_shell, :info, ["* updating public/names"]} - assert_received {:mix_shell, :info, ["* updating public/versions"]} - refute_received _ - - assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) - assert [%{name: "foo", updated_at: _}] = names - assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) - assert versions == [%{name: "foo", retired: [], versions: ["0.9.0", "0.10.0", "1.0.0-rc"]}] - - # Re-generating private key - 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") - flush() - Mix.Task.reenable("hex.registry") - - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - assert_received {:mix_shell, :info, ["* public key at public/public_key does not" <> _]} - assert_received {:mix_shell, :info, ["* updating public/public_key"]} - assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} - assert_received {:mix_shell, :info, ["* updating public/names"]} - assert_received {:mix_shell, :info, ["* updating public/versions"]} - refute_received _ - - # Package with deps - metadata = %{ - name: "bar", - version: "0.1.0", - requirements: %{ - "foo" => %{ - "app" => "foo", - "optional" => false, - "repository" => "acme", - "requirement" => "~> 0.1.0" - }, - "baz" => %{ - "app" => "baz", - "optional" => false, - "repository" => "external", - "requirement" => "~> 0.1.0" + 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + flush() + + {:ok, %{tarball: tarball}} = + :mix_hex_tarball.create(%{name: "foo", version: "0.10.0"}, []) + + File.mkdir!("subdir") + File.write!("subdir/foo-0.10.0.tar", tarball) + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(add public --name acme --private-key private_key.pem subdir/foo-0.10.0.tar) + ) + + assert_received {:mix_shell, :info, ["* reading public/names"]} + assert_received {:mix_shell, :info, ["* reading public/versions"]} + + assert_received {:mix_shell, :info, + ["* moving subdir/foo-0.10.0.tar -> public/tarballs/foo-0.10.0.tar"]} + + assert_received {:mix_shell, :info, ["* skipping public/packages/foo"]} + assert_received {:mix_shell, :info, ["* creating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + config = %{ + :mix_hex_core.default_config() + | repo_url: "http://localhost:#{bypass.port}", + repo_verify: false, + repo_verify_origin: false + } + + assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) + assert [%{name: "foo", updated_at: %{seconds: updated_at}}] = names + + assert updated_at == + "public/tarballs/foo-0.10.0.tar" + |> File.stat!() + |> Map.fetch!(:mtime) + |> Mix.Tasks.Hex.Registry.to_unix() + + assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) + assert versions == [%{name: "foo", retired: [], versions: ["0.10.0"]}] + end) + end + + test "adds a single package to a populated registry" do + in_tmp(fn -> + bypass = setup_bypass() + + {:ok, %{tarball: tarball}} = + :mix_hex_tarball.create(%{name: "foo", version: "0.10.0"}, []) + + File.mkdir_p!("public/tarballs") + File.write!("public/tarballs/foo-0.10.0.tar", tarball) + 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + flush() + + {:ok, %{tarball: tarball}} = :mix_hex_tarball.create(%{name: "foo", version: "0.9.0"}, []) + File.mkdir!("subdir") + File.write!("subdir/foo-0.9.0.tar", tarball) + + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(add public --name acme --private-key private_key.pem subdir/foo-0.9.0.tar) + ) + + assert_received {:mix_shell, :info, ["* reading public/names"]} + assert_received {:mix_shell, :info, ["* reading public/versions"]} + + assert_received {:mix_shell, :info, + ["* moving subdir/foo-0.9.0.tar -> public/tarballs/foo-0.9.0.tar"]} + + assert_received {:mix_shell, :info, ["* reading public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + config = %{ + :mix_hex_core.default_config() + | repo_url: "http://localhost:#{bypass.port}", + repo_verify: false, + repo_verify_origin: false + } + + assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) + assert [%{name: "foo", updated_at: %{seconds: updated_at}}] = names + + assert updated_at == + "public/tarballs/foo-0.9.0.tar" + |> File.stat!() + |> Map.fetch!(:mtime) + |> Mix.Tasks.Hex.Registry.to_unix() + + assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) + assert versions == [%{name: "foo", retired: [], versions: ["0.9.0", "0.10.0"]}] + end) + end + + test "adds multiple packages" do + in_tmp(fn -> + bypass = setup_bypass() + + {:ok, %{tarball: tarball}} = + :mix_hex_tarball.create(%{name: "foo", version: "0.10.0"}, []) + + File.mkdir_p!("public/tarballs") + File.write!("public/tarballs/foo-0.10.0.tar", tarball) + 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + flush() + + {:ok, %{tarball: bar_tarball}} = + :mix_hex_tarball.create(%{name: "bar", version: "0.1.0"}, []) + + {:ok, %{tarball: foo_tarball}} = + :mix_hex_tarball.create(%{name: "foo", version: "0.9.0"}, []) + + File.mkdir!("subdir") + File.write!("subdir/bar-0.1.0.tar", bar_tarball) + File.write!("subdir/foo-0.9.0.tar", foo_tarball) + + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(add public --name acme --private-key private_key.pem subdir/foo-0.9.0.tar subdir/bar-0.1.0.tar) + ) + + assert_received {:mix_shell, :info, ["* reading public/names"]} + assert_received {:mix_shell, :info, ["* reading public/versions"]} + + assert_received {:mix_shell, :info, + ["* moving subdir/foo-0.9.0.tar -> public/tarballs/foo-0.9.0.tar"]} + + assert_received {:mix_shell, :info, + ["* moving subdir/bar-0.1.0.tar -> public/tarballs/bar-0.1.0.tar"]} + + assert_received {:mix_shell, :info, ["* reading public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* skipping public/packages/bar"]} + assert_received {:mix_shell, :info, ["* creating public/packages/bar"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + config = %{ + :mix_hex_core.default_config() + | repo_url: "http://localhost:#{bypass.port}", + repo_verify: false, + repo_verify_origin: false + } + + assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) + + assert [ + %{name: "bar", updated_at: %{seconds: bar_updated_at}}, + %{name: "foo", updated_at: %{seconds: foo_updated_at}} + ] = names + + assert bar_updated_at == + "public/tarballs/bar-0.1.0.tar" + |> File.stat!() + |> Map.fetch!(:mtime) + |> Mix.Tasks.Hex.Registry.to_unix() + + assert foo_updated_at == + "public/tarballs/foo-0.9.0.tar" + |> File.stat!() + |> Map.fetch!(:mtime) + |> Mix.Tasks.Hex.Registry.to_unix() + + assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) + + assert versions == [ + %{name: "bar", retired: [], versions: ["0.1.0"]}, + %{name: "foo", retired: [], versions: ["0.9.0", "0.10.0"]} + ] + end) + end + end + + describe "build" do + test "builds a registry" do + in_tmp(fn -> + bypass = setup_bypass() + + 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") + flush() + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + assert_received {:mix_shell, :info, ["* creating public/public_key"]} + assert_received {:mix_shell, :info, ["* creating public/tarballs"]} + assert_received {:mix_shell, :info, ["* creating public/names"]} + assert_received {:mix_shell, :info, ["* creating public/versions"]} + refute_received _ + + config = %{ + :mix_hex_core.default_config() + | repo_url: "http://localhost:#{bypass.port}", + repo_verify: false, + repo_verify_origin: false + } + + assert {:ok, {200, _, []}} = :mix_hex_repo.get_names(config) + assert {:ok, {200, _, []}} = :mix_hex_repo.get_versions(config) + + {:ok, %{tarball: tarball}} = + :mix_hex_tarball.create(%{name: "foo", version: "0.10.0"}, []) + + File.write!("public/tarballs/foo-0.10.0.tar", tarball) + + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + assert_received {:mix_shell, :info, ["* creating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) + assert [%{name: "foo", updated_at: %{seconds: updated_at}}] = names + + assert updated_at == + "public/tarballs/foo-0.10.0.tar" + |> File.stat!() + |> Map.fetch!(:mtime) + |> Mix.Tasks.Hex.Registry.to_unix() + + assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) + assert versions == [%{name: "foo", retired: [], versions: ["0.10.0"]}] + + {:ok, %{tarball: tarball}} = :mix_hex_tarball.create(%{name: "foo", version: "0.9.0"}, []) + File.write!("public/tarballs/foo-0.9.0.tar", tarball) + + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) + assert [%{name: "foo", updated_at: %{seconds: updated_at}}] = names + + assert updated_at == + "public/tarballs/foo-0.9.0.tar" + |> File.stat!() + |> Map.fetch!(:mtime) + |> Mix.Tasks.Hex.Registry.to_unix() + + assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) + assert versions == [%{name: "foo", retired: [], versions: ["0.9.0", "0.10.0"]}] + + # Versions with hyphen + {:ok, %{tarball: tarball}} = + :mix_hex_tarball.create(%{name: "foo", version: "1.0.0-rc"}, []) + + File.write!("public/tarballs/foo-1.0.0-rc.tar", tarball) + + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) + assert [%{name: "foo", updated_at: _}] = names + assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) + + assert versions == [ + %{name: "foo", retired: [], versions: ["0.9.0", "0.10.0", "1.0.0-rc"]} + ] + + # Re-generating private key + 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") + flush() + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + assert_received {:mix_shell, :info, ["* public key at public/public_key does not" <> _]} + assert_received {:mix_shell, :info, ["* updating public/public_key"]} + assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + # Package with deps + metadata = %{ + name: "bar", + version: "0.1.0", + requirements: %{ + "foo" => %{ + "app" => "foo", + "optional" => false, + "repository" => "acme", + "requirement" => "~> 0.1.0" + }, + "baz" => %{ + "app" => "baz", + "optional" => false, + "repository" => "external", + "requirement" => "~> 0.1.0" + } } } - } - - {:ok, %{tarball: tarball}} = :mix_hex_tarball.create(metadata, []) - File.write!("public/tarballs/bar-0.1.0.tar", tarball) - - Mix.Task.reenable("hex.registry") - - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - assert_received {:mix_shell, :info, ["* creating public/packages/bar"]} - assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} - assert_received {:mix_shell, :info, ["* updating public/names"]} - assert_received {:mix_shell, :info, ["* updating public/versions"]} - refute_received _ - - assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) - assert [%{name: "bar", updated_at: _}, %{name: "foo", updated_at: _}] = names - assert {:ok, {200, _, [package]}} = :mix_hex_repo.get_package(config, "bar") - - assert package.dependencies == [ - %{ - app: "baz", - optional: false, - package: "baz", - requirement: "~> 0.1.0", - repository: "external" - }, - %{ - app: "foo", - optional: false, - package: "foo", - requirement: "~> 0.1.0" - } - ] - - # Removing all package releases - File.rm!("public/tarballs/foo-0.9.0.tar") - File.rm!("public/tarballs/foo-0.10.0.tar") - File.rm!("public/tarballs/foo-1.0.0-rc.tar") - Mix.Task.reenable("hex.registry") - - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - assert_received {:mix_shell, :info, ["* updating public/packages/bar"]} - assert_received {:mix_shell, :info, ["* removing public/packages/foo"]} - assert_received {:mix_shell, :info, ["* updating public/names"]} - assert_received {:mix_shell, :info, ["* updating public/versions"]} - refute_received _ - end) + + {:ok, %{tarball: tarball}} = :mix_hex_tarball.create(metadata, []) + File.write!("public/tarballs/bar-0.1.0.tar", tarball) + + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + assert_received {:mix_shell, :info, ["* creating public/packages/bar"]} + assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) + assert [%{name: "bar", updated_at: _}, %{name: "foo", updated_at: _}] = names + assert {:ok, {200, _, [package]}} = :mix_hex_repo.get_package(config, "bar") + + assert package.dependencies == [ + %{ + app: "baz", + optional: false, + package: "baz", + requirement: "~> 0.1.0", + repository: "external" + }, + %{ + app: "foo", + optional: false, + package: "foo", + requirement: "~> 0.1.0" + } + ] + + # Removing all package releases + File.rm!("public/tarballs/foo-0.9.0.tar") + File.rm!("public/tarballs/foo-0.10.0.tar") + File.rm!("public/tarballs/foo-1.0.0-rc.tar") + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + assert_received {:mix_shell, :info, ["* updating public/packages/bar"]} + assert_received {:mix_shell, :info, ["* removing public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + end) + end end defp setup_bypass() do