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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
require_relative "../../test_helper"

require "rack/test"

require "pro_tacts/profile"
require "pro_tacts/web"

# GET /setup and the document it links to, exercised the way the other
# screens are: real requests through the Roda app. The profile is
# rendered from the request alone, so only naming the book needs a
# database standing behind it.
class AdminDeviceSetupTest < Minitest::Test
  include Rack::Test::Methods
  include ThrowawayContacts

  def app
    ProTacts::Web
  end

  def setup
    header "Remote-User", "test@example.com"
    header "Host", "box.example.ts.net"
  end

  # The account name is read from the environment, so a name set in the
  # shell running the suite would otherwise decide what these assert.
  def with_config(env = {})
    original = ProTacts.config
    ProTacts.config = ProTacts::Config.new(env)
    yield
  ensure
    ProTacts.config = original
  end

  def test_screen_names_the_account_the_download_carries
    with_config do
      get "/setup"

      assert_equal 200, last_response.status
      assert_equal "text/html; charset=utf-8", last_response["Content-Type"]
      assert_includes last_response.body, "box.example.ts.net"
      assert_includes last_response.body, %(<dd class="type-body-sm">pro-tacts</dd>)
      assert_includes last_response.body, "/setup/carddav.mobileconfig"
    end
  end

  # The screen states what the document is about to carry, so a rename
  # reaching only one of the two would put a name on the screen that the
  # installed account does not have.
  def test_the_configured_name_reaches_the_screen_and_the_document
    with_config("PRO_TACTS_INSTANCE_NAME" => "pro-tacts (dev)") do
      get "/setup"

      assert_includes last_response.body, %(<dd class="type-body-sm">pro-tacts (dev)</dd>)

      get "/setup/carddav.mobileconfig"

      assert_includes last_response.body, "<string>pro-tacts (dev)</string>"
    end
  end

  # The media type is what makes a device offer to install rather than
  # save, so it is the assertion that matters most here.
  def test_document_is_served_as_a_configuration_profile
    get "/setup/carddav.mobileconfig"

    assert_equal 200, last_response.status
    assert_equal "application/x-apple-aspen-config", last_response["Content-Type"]
    assert_includes last_response.body, "<string>com.apple.carddav.account</string>"
  end

  # The point of reading the hostname off the request: whatever address
  # the device reached us at is the address the account is provisioned
  # for.
  def test_document_names_the_host_the_request_arrived_on
    header "Host", "other.example.ts.net"
    get "/setup/carddav.mobileconfig"

    assert_includes last_response.body, "<string>other.example.ts.net</string>"
  end

  def test_document_names_the_requester_as_the_username
    header "Remote-User", "zoe@example.com"
    get "/setup/carddav.mobileconfig"

    assert_match(%r{<key>CardDAVUsername</key>\s*<string>zoe@example.com</string>}, last_response.body)
  end

  # Fresh per download (see Profile): a reinstall is a cold account, not
  # an update of the one already there.
  def test_each_download_carries_its_own_identifier
    get "/setup/carddav.mobileconfig"
    first = last_response.body
    get "/setup/carddav.mobileconfig"

    refute_equal first, last_response.body
    assert_includes first, ProTacts::Profile::IDENTIFIER_PREFIX
    assert_includes last_response.body, ProTacts::Profile::IDENTIFIER_PREFIX
  end

  ## Naming the book

  def test_naming_the_book_names_the_requesters_own
    with_contacts({}) do |store|
      id = store.create_group(name: "sync:test@example.com")

      post "/setup/book", name: "test"

      assert_equal 200, last_response.status
      assert_equal "test@example.com syncs sync:test\n", last_response.body
      assert_equal "sync:test", store.group(id).name

      post "/setup/book"

      assert_equal "test@example.com syncs sync:test@example.com\n", last_response.body
      assert_equal "test@example.com", store.book_name("test@example.com")
    end
  end

  def test_a_taken_book_name_is_a_conflict
    with_contacts({}) do |store|
      store.name_book("zoe@example.com", "zoe")

      post "/setup/book", name: "zoe"

      assert_equal 409, last_response.status
      assert_equal "sync:zoe is taken; test@example.com's book is unchanged.\n", last_response.body
      assert_equal "test@example.com", store.book_name("test@example.com")

      post "/setup/book", name: "*"

      assert_equal 409, last_response.status
    end
  end
end