1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
require "pathname"
require "tmpdir"

$LOAD_PATH.unshift(Pathname.new(__dir__) / "lib")
require "minitest/test_task"

Minitest::TestTask.create

Dir.glob("tasks/*.rake").sort.each do
  import it
end

desc "Start development server, reloading on changes"
task :dev do
  # The dev server serves the fixture book demo.ru seeds (see there for
  # when it reseeds), so a client sees known state and the real data/
  # directory stays out of the dev loop. The data lives in a
  # session-scoped tmpdir: two servers running at once each get their
  # own database, and the directory goes when the task does.
  # entr's watch list is fd's snapshot at launch: a file created
  # after `rake dev` starts is never watched, and edits to it never
  # reload the server — restart the task when work adds a file.
  # A dev session's exchanges go to their own log, truncated per start,
  # so reading a session back never means picking it out of older ones —
  # the data directory's log/exchange.log stays the default. An exported
  # PRO_TACTS_EXCHANGE_LOG (e.g. stderr) wins over both.
  ENV["PRO_TACTS_EXCHANGE_LOG"] ||= "log/dev.log"
  File.truncate("log/dev.log", 0) if File.exist?("log/dev.log")

  Dir.mktmpdir("pro-tacts-dev") do |dir|
    ENV["PRO_TACTS_DATA_DIR"] = dir
    sh "fd -e rb . lib | entr -r rackup -o localhost dev.ru"
  end
end

desc "Regenerate the recorded exchange response fixtures from current responses"
task :fixtures do
  # Mirrors test/test_helper.rb, which cannot be required here without
  # minitest/autorun running its at_exit hook inside rake. Its own
  # tmpdir, so a fixture re-record cannot race a concurrent test run.
  # The exchange log dies with that tmpdir too: the fixture files are the
  # durable record of a re-record, and a replay's exchanges must not
  # append to a log a real client session also uses.
  Dir.mktmpdir("pro-tacts-test") do |dir|
    data_dir = Pathname.new(dir)
    ENV["PRO_TACTS_DATA_DIR"] = data_dir.to_s
    ENV["PRO_TACTS_EXCHANGE_LOG"] ||= (data_dir / "exchange.log").to_s
    require "pro_tacts/web"
    require_relative "test/fixture_data"
    require_relative "test/pro_tacts/exchange_fixtures"

    # A store per recording, seeded fresh: the iOS session ends by
    # deleting a card, and the recording after it must not start from
    # what that one left behind.
    ExchangeFixtures.all.each do |recording|
      store = FixtureData.install(data_dir / recording.directory.basename, genesis: ExchangeFixtures::GENESIS)
      ProTacts::Web.store = store
      recording.record_responses(ProTacts::Web)
      store.close
    end
  end
end

desc "Type check lib against the RBS comments in it"
task :steep do
  sh "steep", "check"
end

namespace :profile do
  # The profile comes from the running app rather than a render here: the
  # route is the renderer (ProTacts::Web's setup branch), it writes the
  # requester's own login into the account where a render here had no
  # request to read one from, and a host that answers the download is a
  # host the account can reach. The download needs the identity header
  # the app reads, so it works against a deployment behind a proxy that
  # writes it and not against a server with nothing in front.
  #
  # Downloaded before the sweep rather than after, so a download that
  # fails leaves the installed account where it was.
  desc "Download the profile from PRO_TACTS_HOSTNAME and stage it for approval"
  task :install do
    sh "curl", "--fail", "--silent", "--show-error", "--location",
      "--output", "carddav.mobileconfig",
      "https://#{ENV.fetch("PRO_TACTS_HOSTNAME")}/setup/carddav.mobileconfig"

    Rake::Task["profile:remove"].invoke

    sh "open", "carddav.mobileconfig"
    sh "open", "x-apple.systempreferences:com.apple.preferences.configurationprofiles"
  end

  desc "Remove the installed configuration profiles pointing at PRO_TACTS_HOSTNAME"
  task :remove do
    require "pro_tacts/profile"

    list = `profiles list`
    hostname = ENV.fetch("PRO_TACTS_HOSTNAME")
    identifiers = ProTacts::Profile.installed_identifiers(list, hostname:)
    if identifiers.empty?
      puts "No #{hostname} profiles found; remove by hand in System Settings → Profiles if one lingers."
    else
      identifiers.each do
        sh "profiles", "remove", "-identifier", it
      end
    end

    # A profile pointing at another host provisions another server's
    # account — the deployment's, beside a dev one — and removing it would
    # take that account down with it. A profile from before the host was
    # part of the identifier says nothing about where it points, so it is
    # reported rather than swept.
    others = ProTacts::Profile.installed_identifiers(list, hostname: nil) - identifiers
    unless others.empty?
      puts "Left installed, from another host or an older identifier (remove by hand if stale):", *others
    end
  end
end

task default: %i[ steep test ]