appship 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +164 -18
- data/lib/appship/builder.rb +48 -39
- data/lib/appship/cache.rb +55 -19
- data/lib/appship/cli.rb +143 -50
- data/lib/appship/config.rb +18 -2
- data/lib/appship/default_message +9 -0
- data/lib/appship/message.rb +193 -0
- data/lib/appship/project_config.rb +115 -0
- data/lib/appship/uploader.rb +26 -10
- data/lib/appship/version.rb +1 -1
- data/lib/appship/webhook.rb +151 -0
- data/lib/appship.rb +3 -0
- data/test/test_cache.rb +263 -0
- data/test/test_cli.rb +356 -0
- data/test/test_display.rb +120 -0
- data/test/test_message.rb +138 -0
- data/test/test_project_config.rb +221 -0
- data/test/test_upload_progress.rb +110 -0
- data/test/test_webhook.rb +352 -0
- metadata +13 -4
data/test/test_cli.rb
CHANGED
|
@@ -1,9 +1,365 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "minitest/autorun"
|
|
4
|
+
require "stringio"
|
|
4
5
|
require_relative "../lib/appship"
|
|
5
6
|
|
|
6
7
|
class CLITest < Minitest::Test
|
|
8
|
+
def setup
|
|
9
|
+
@cli_home = Dir.mktmpdir("appship-cli-")
|
|
10
|
+
@original_home = ENV["HOME"]
|
|
11
|
+
ENV["HOME"] = @cli_home
|
|
12
|
+
@project_dir = File.join(@cli_home, "project")
|
|
13
|
+
FileUtils.mkdir_p(File.join(@project_dir, "Demo.xcodeproj"))
|
|
14
|
+
@cache_path = File.join(@cli_home, ".config")
|
|
15
|
+
@profiles = [
|
|
16
|
+
{ "project_name" => "Demo", "app_name" => "DemoApp" },
|
|
17
|
+
{ "project_name" => "Other", "custom" => "keep" }
|
|
18
|
+
]
|
|
19
|
+
File.write(@cache_path, JSON.generate(@profiles))
|
|
20
|
+
@original_stdin = $stdin
|
|
21
|
+
$stdin = StringIO.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def teardown
|
|
25
|
+
$stdin = @original_stdin
|
|
26
|
+
ENV["HOME"] = @original_home
|
|
27
|
+
FileUtils.remove_entry(@cli_home)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class FailingBuilder
|
|
31
|
+
attr_reader :calls, :options
|
|
32
|
+
|
|
33
|
+
def initialize(error)
|
|
34
|
+
@error = error
|
|
35
|
+
@calls = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def find_cached_app!(_options)
|
|
39
|
+
@calls << :find_cached_app
|
|
40
|
+
raise @error
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build!(options)
|
|
44
|
+
@options = options
|
|
45
|
+
@calls << :build
|
|
46
|
+
raise @error
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def package!(options)
|
|
50
|
+
@options = options
|
|
51
|
+
@calls << :package
|
|
52
|
+
raise @error
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def with_release_fakes(error, notification_error: nil)
|
|
57
|
+
original_builder = Appship::Builder.method(:new)
|
|
58
|
+
original_webhook = Appship::Webhook.method(:new)
|
|
59
|
+
builder = FailingBuilder.new(error)
|
|
60
|
+
messages = []
|
|
61
|
+
Appship::Builder.define_singleton_method(:new) { |*| builder }
|
|
62
|
+
Appship::Webhook.define_singleton_method(:new) do |options|
|
|
63
|
+
webhook = original_webhook.call(options)
|
|
64
|
+
webhook.define_singleton_method(:notify) do |message, failure: false|
|
|
65
|
+
messages << { message: message, failure: failure }
|
|
66
|
+
raise notification_error if notification_error
|
|
67
|
+
end
|
|
68
|
+
webhook
|
|
69
|
+
end
|
|
70
|
+
yield builder, messages
|
|
71
|
+
ensure
|
|
72
|
+
Appship::Builder.define_singleton_method(:new, original_builder)
|
|
73
|
+
Appship::Webhook.define_singleton_method(:new, original_webhook)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def configured_release_args(provider = "pgyer")
|
|
77
|
+
@profiles.first.merge!(
|
|
78
|
+
"pgyer_api_key" => "pgyer-test-key", "pgyer_password" => "",
|
|
79
|
+
"fir_api_key" => "fir-test-key", "fir_password" => "",
|
|
80
|
+
"wechat_webhook" => "https://example.test/send?key=test-key"
|
|
81
|
+
)
|
|
82
|
+
File.write(@cache_path, JSON.generate(@profiles))
|
|
83
|
+
[provider, "--project-dir", @project_dir, "--env-file", @cache_path, "--description", "test", "--no-badge"]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_missing_cache_stops_both_platforms_and_sends_one_failure_notification
|
|
87
|
+
%w[pgyer fir].each do |provider|
|
|
88
|
+
args = configured_release_args(provider) + ["--cache"]
|
|
89
|
+
error = Appship::CommandError.new("未找到缓存 App")
|
|
90
|
+
with_release_fakes(error) do |builder, messages|
|
|
91
|
+
capture_io do
|
|
92
|
+
actual = assert_raises(Appship::CommandError) { Appship::CLI.new.start(args) }
|
|
93
|
+
assert_same error, actual
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
assert_equal [:find_cached_app], builder.calls
|
|
97
|
+
assert_equal 1, messages.length
|
|
98
|
+
assert messages.first[:failure]
|
|
99
|
+
assert_includes messages.first[:message], "未找到缓存 App"
|
|
100
|
+
assert_includes messages.first[:message], "DemoApp"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_missing_credentials_can_notify_using_the_matching_project_webhook
|
|
106
|
+
args = configured_release_args + ["--rebuild"]
|
|
107
|
+
@profiles.first.delete("pgyer_api_key")
|
|
108
|
+
File.write(@cache_path, JSON.generate(@profiles))
|
|
109
|
+
with_release_fakes(Appship::CommandError.new("must not build")) do |builder, messages|
|
|
110
|
+
capture_io do
|
|
111
|
+
assert_raises(Appship::ConfigurationError) { Appship::CLI.new.start(args) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
assert_empty builder.calls
|
|
115
|
+
assert_equal 1, messages.length
|
|
116
|
+
assert_includes messages.first[:message], "缓存缺少 pgyer_api_key"
|
|
117
|
+
assert_includes messages.first[:message], "DemoApp"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_both_release_commands_use_partial_project_config_and_persist_credentials_locally
|
|
122
|
+
%w[pgyer fir].each do |provider|
|
|
123
|
+
args = configured_release_args(provider) + ["--rebuild", "-key", "local-key", "-password", ""]
|
|
124
|
+
local_path = Appship::ProjectConfig.path(@project_dir)
|
|
125
|
+
FileUtils.mkdir_p(File.dirname(local_path))
|
|
126
|
+
File.write(local_path, YAML.dump("project_name" => "Demo", "app_name" => "LocalApp", "configuration" => "Release",
|
|
127
|
+
"app_icon_badge" => true, "badge" => "QA"))
|
|
128
|
+
global_content = File.read(@cache_path)
|
|
129
|
+
|
|
130
|
+
with_release_fakes(Appship::CommandError.new("stop before archive")) do |builder, messages|
|
|
131
|
+
capture_io { assert_raises(Appship::CommandError) { Appship::CLI.new.start(args) } }
|
|
132
|
+
|
|
133
|
+
assert_equal [:build], builder.calls
|
|
134
|
+
assert_equal "LocalApp", builder.options[:app_name]
|
|
135
|
+
assert_equal "Release", builder.options[:configuration]
|
|
136
|
+
assert_nil builder.options[:badge]
|
|
137
|
+
assert_equal "local-key", builder.options[provider == "fir" ? :fir_api_token : :api_key]
|
|
138
|
+
assert_equal "", builder.options[provider == "fir" ? :fir_password : :password]
|
|
139
|
+
assert_equal @profiles.first["wechat_webhook"], builder.options[:wechat_webhook]
|
|
140
|
+
assert_equal 1, messages.length
|
|
141
|
+
assert_includes messages.first[:message], "LocalApp"
|
|
142
|
+
end
|
|
143
|
+
saved = YAML.safe_load_file(local_path)
|
|
144
|
+
assert_equal "local-key", saved["#{provider}_api_key"]
|
|
145
|
+
assert_equal "", saved["#{provider}_password"]
|
|
146
|
+
refute saved.key?(provider == "fir" ? "pgyer_api_key" : "fir_api_key")
|
|
147
|
+
assert_equal global_content, File.read(@cache_path)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def test_app_icon_badge_settings_override_automatic_choices
|
|
152
|
+
cli = Appship::CLI.new
|
|
153
|
+
cases = [
|
|
154
|
+
[{ "app_icon_badge" => false, "app_icon_badge_environment" => true, "badge" => "QA" }, nil],
|
|
155
|
+
[{ "app_icon_badge" => true }, "RELEASE"],
|
|
156
|
+
[{ "app_icon_badge_environment" => true }, "RELEASE"],
|
|
157
|
+
[{ "app_icon_badge" => true, "app_icon_badge_environment" => false, "app_icon_badge_version" => true }, nil],
|
|
158
|
+
[{ "app_icon_badge_environment" => false, "badge" => "QA" }, "QA"]
|
|
159
|
+
]
|
|
160
|
+
cases.each do |profile, expected|
|
|
161
|
+
options = { configuration: "Release" }
|
|
162
|
+
cli.send(:apply_badge_config!, options, profile) { flunk "should not prompt for a badge" }
|
|
163
|
+
expected ? assert_equal(expected, options[:badge]) : assert_nil(options[:badge])
|
|
164
|
+
end
|
|
165
|
+
assert_raises(Appship::ConfigurationError) do
|
|
166
|
+
cli.send(:apply_badge_config!, {}, { "app_icon_badge" => "false" })
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def test_cache_badge_uses_configuration_of_the_package_found
|
|
171
|
+
cli = Appship::CLI.new
|
|
172
|
+
builder = Object.new
|
|
173
|
+
builder.define_singleton_method(:find_cached_app!) do |options|
|
|
174
|
+
options[:configuration] = "Release"
|
|
175
|
+
"Demo.app"
|
|
176
|
+
end
|
|
177
|
+
options = { build_mode: "cache", build_mode_explicit: true, configuration: "Debug", description: "test" }
|
|
178
|
+
|
|
179
|
+
capture_io { cli.send(:prepare_pgyer_flow!, options, builder, { "app_icon_badge" => true }) }
|
|
180
|
+
|
|
181
|
+
assert_equal "Demo.app", options[:app]
|
|
182
|
+
assert_equal "RELEASE", options[:badge]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def test_build_and_package_resolve_badges_after_cli_options
|
|
186
|
+
path = File.join(@cli_home, "config.yml")
|
|
187
|
+
File.write(path, YAML.dump("configuration" => "Debug", "app_icon_badge" => true))
|
|
188
|
+
|
|
189
|
+
[
|
|
190
|
+
[%w[build --configuration Release], "RELEASE"],
|
|
191
|
+
[%w[package --no-badge], nil],
|
|
192
|
+
[%w[package], "DEBUG"],
|
|
193
|
+
[%w[package --badge QA], "QA"]
|
|
194
|
+
].each do |command, expected|
|
|
195
|
+
with_release_fakes(Appship::CommandError.new("stop before build")) do |builder, _messages|
|
|
196
|
+
capture_io { assert_raises(Appship::CommandError) { Appship::CLI.new.start(["--config", path, *command]) } }
|
|
197
|
+
expected ? assert_equal(expected, builder.options[:badge]) : assert_nil(builder.options[:badge])
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def test_missing_credentials_can_notify_from_local_only_profile
|
|
203
|
+
args = ["pgyer", "--project-dir", @project_dir, "--env-file", File.join(@cli_home, "missing.config"), "--rebuild"]
|
|
204
|
+
local_path = Appship::ProjectConfig.path(@project_dir)
|
|
205
|
+
FileUtils.mkdir_p(File.dirname(local_path))
|
|
206
|
+
File.write(local_path, JSON.generate("project_name" => "Demo", "app_name" => "LocalApp", "feishu_webhook" => "https://example.test/local"))
|
|
207
|
+
|
|
208
|
+
with_release_fakes(Appship::CommandError.new("must not build")) do |builder, messages|
|
|
209
|
+
capture_io { assert_raises(Appship::ConfigurationError) { Appship::CLI.new.start(args) } }
|
|
210
|
+
|
|
211
|
+
assert_empty builder.calls
|
|
212
|
+
assert_equal 1, messages.length
|
|
213
|
+
assert_includes messages.first[:message], "缺少 pgyer_api_key"
|
|
214
|
+
assert_includes messages.first[:message], "LocalApp"
|
|
215
|
+
end
|
|
216
|
+
refute File.exist?(File.join(@cli_home, "missing.config"))
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def test_unexpected_exception_and_user_interruption_notify_and_keep_original_error
|
|
220
|
+
[RuntimeError.new("unexpected failure"), Interrupt.new("cancelled")].each do |error|
|
|
221
|
+
args = configured_release_args + ["--rebuild", "--configuration", "Release"]
|
|
222
|
+
with_release_fakes(error) do |builder, messages|
|
|
223
|
+
capture_io do
|
|
224
|
+
actual = assert_raises(error.class) { Appship::CLI.new.start(args) }
|
|
225
|
+
assert_same error, actual
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
assert_equal [:build], builder.calls
|
|
229
|
+
assert_equal 1, messages.length
|
|
230
|
+
assert messages.first[:failure]
|
|
231
|
+
assert_includes messages.first[:message], error.message
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def test_failure_notification_error_never_replaces_the_release_error
|
|
237
|
+
args = configured_release_args + ["--cache"]
|
|
238
|
+
error = Appship::CommandError.new("cache failed")
|
|
239
|
+
with_release_fakes(error, notification_error: IOError.new("webhook failed")) do |_builder, messages|
|
|
240
|
+
_output, warnings = capture_io do
|
|
241
|
+
actual = assert_raises(Appship::CommandError) { Appship::CLI.new.start(args) }
|
|
242
|
+
assert_same error, actual
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
assert_equal 1, messages.length
|
|
246
|
+
assert_includes warnings, "保留原始错误"
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def test_failed_project_matching_does_not_notify_other_projects
|
|
251
|
+
args = configured_release_args + ["--cache"]
|
|
252
|
+
@profiles = [{ "project_name" => "Other", "wechat_webhook" => "https://example.test/other" }]
|
|
253
|
+
File.write(@cache_path, JSON.generate(@profiles))
|
|
254
|
+
|
|
255
|
+
with_release_fakes(Appship::CommandError.new("must not build")) do |builder, messages|
|
|
256
|
+
capture_io { assert_raises(Appship::ConfigurationError) { Appship::CLI.new.start(args) } }
|
|
257
|
+
|
|
258
|
+
assert_empty messages
|
|
259
|
+
assert_empty builder.calls
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def test_webhook_settings_save_without_credentials_or_exposing_urls
|
|
264
|
+
{
|
|
265
|
+
"wechat" => "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=test-key",
|
|
266
|
+
"dingtalk" => "https://oapi.dingtalk.com/robot/send?access_token=test-token",
|
|
267
|
+
"feishu" => "https://open.feishu.cn/open-apis/bot/v2/hook/test-token"
|
|
268
|
+
}.each do |platform, url|
|
|
269
|
+
output, errors = capture_io do
|
|
270
|
+
Appship::CLI.new.start(["webhook", platform, url, "--project-dir", @project_dir, "--env-file", @cache_path])
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
saved = JSON.parse(File.read(@cache_path))
|
|
274
|
+
assert_equal url, saved.first["#{platform}_webhook"]
|
|
275
|
+
assert_equal @profiles.last, saved.last
|
|
276
|
+
assert_equal 0o600, File.stat(@cache_path).mode & 0o777
|
|
277
|
+
assert_includes output, "已保存"
|
|
278
|
+
refute_includes output + errors, url
|
|
279
|
+
refute_includes output, "请输入"
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def test_webhook_empty_url_disables_only_the_selected_project
|
|
284
|
+
@profiles.first["wechat_webhook"] = "https://example.test/old"
|
|
285
|
+
File.write(@cache_path, JSON.generate(@profiles))
|
|
286
|
+
|
|
287
|
+
capture_io do
|
|
288
|
+
Appship::CLI.new.start(["webhook", "wechat", "", "--project-name", "Demo", "--env-file", @cache_path])
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
saved = JSON.parse(File.read(@cache_path))
|
|
292
|
+
assert_equal "", saved.first["wechat_webhook"]
|
|
293
|
+
assert_equal @profiles.last, saved.last
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def test_invalid_webhook_settings_do_not_modify_cache
|
|
297
|
+
[
|
|
298
|
+
["wechat"],
|
|
299
|
+
["unknown", "https://example.test"],
|
|
300
|
+
["wechat", "http://example.test"],
|
|
301
|
+
["wechat", "https://user:password@example.test"],
|
|
302
|
+
["wechat", "https://example.test/#secret"],
|
|
303
|
+
["wechat", "not a url"]
|
|
304
|
+
].each do |args|
|
|
305
|
+
original = File.read(@cache_path)
|
|
306
|
+
capture_io do
|
|
307
|
+
assert_raises(Appship::ConfigurationError) do
|
|
308
|
+
Appship::CLI.new.start(["webhook", *args, "--env-file", @cache_path])
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
assert_equal original, File.read(@cache_path)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def test_pgyer_profile_passes_own_credentials_and_webhooks_to_upload_options
|
|
316
|
+
options = {}
|
|
317
|
+
profile = { "project_name" => "Demo", "app_name" => "DemoApp", "pgyer_api_key" => "key", "pgyer_password" => "",
|
|
318
|
+
"fir_api_key" => "fir-key", "fir_password" => "fir-password",
|
|
319
|
+
"wechat_webhook" => "https://example.test/wechat", "dingtalk_webhook" => "https://example.test/dingtalk",
|
|
320
|
+
"feishu_webhook" => "https://example.test/feishu", "feishu_webhook_secret" => "signing-secret" }
|
|
321
|
+
|
|
322
|
+
capture_io { Appship::CLI.new.send(:apply_cached_profile!, options, profile) }
|
|
323
|
+
|
|
324
|
+
assert_equal "key", options[:api_key]
|
|
325
|
+
assert_equal "", options[:password]
|
|
326
|
+
assert_equal 1, options[:install_type]
|
|
327
|
+
assert_equal profile["wechat_webhook"], options[:wechat_webhook]
|
|
328
|
+
assert_equal profile["dingtalk_webhook"], options[:dingtalk_webhook]
|
|
329
|
+
assert_equal profile["feishu_webhook"], options[:feishu_webhook]
|
|
330
|
+
assert_equal profile["feishu_webhook_secret"], options[:feishu_webhook_secret]
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def test_unmatched_target_prompts_for_project_configuration
|
|
334
|
+
Dir.mktmpdir do |directory|
|
|
335
|
+
FileUtils.mkdir_p(File.join(directory, "Demo.xcodeproj"))
|
|
336
|
+
config_path = File.join(directory, "custom.config")
|
|
337
|
+
Dir.chdir(directory) do
|
|
338
|
+
error = assert_raises(Appship::ConfigurationError) do
|
|
339
|
+
Appship::CLI.new.send(:infer_pgyer_target!, { project_name: "WMApp", env_file: config_path })
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
assert_includes error.message, config_path
|
|
343
|
+
assert_includes error.message, "增加当前项目配置"
|
|
344
|
+
assert_includes error.message, "project_name: Demo"
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def test_missing_target_prompts_for_project_directory
|
|
350
|
+
Dir.mktmpdir do |directory|
|
|
351
|
+
Dir.chdir(directory) do
|
|
352
|
+
error = assert_raises(Appship::ConfigurationError) do
|
|
353
|
+
Appship::CLI.new.send(:infer_pgyer_target!, { project_name: "WMApp" })
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
assert_includes error.message, "project_name"
|
|
357
|
+
assert_includes error.message, "--project-dir"
|
|
358
|
+
assert_includes error.message, directory
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
7
363
|
def test_fir_short_options_are_normalized
|
|
8
364
|
args = ["-key", "new-key", "-password", "new-password"]
|
|
9
365
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require_relative "../lib/appship"
|
|
5
|
+
|
|
6
|
+
class DisplayTest < Minitest::Test
|
|
7
|
+
def setup
|
|
8
|
+
@home = Dir.mktmpdir("appship-display-")
|
|
9
|
+
@original_home = ENV["HOME"]
|
|
10
|
+
@original_directory = Dir.pwd
|
|
11
|
+
ENV["HOME"] = @home
|
|
12
|
+
Dir.chdir(@home)
|
|
13
|
+
@message_path = Appship::Message.path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def teardown
|
|
17
|
+
Dir.chdir(@original_directory)
|
|
18
|
+
ENV["HOME"] = @original_home
|
|
19
|
+
FileUtils.remove_entry(@home)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_version_does_not_create_message_files
|
|
23
|
+
output, = capture_io { Appship::CLI.new.start(["--version"]) }
|
|
24
|
+
|
|
25
|
+
assert_equal "appship #{Appship::VERSION}\n", output
|
|
26
|
+
refute File.exist?(@message_path)
|
|
27
|
+
refute File.exist?(File.join(@home, ".appship"))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_missing_project_template_uses_bundle_without_reading_or_writing_global_messages
|
|
31
|
+
legacy_path = File.join(@home, ".appship", "message.txt")
|
|
32
|
+
FileUtils.mkdir_p(File.dirname(legacy_path))
|
|
33
|
+
File.write(legacy_path, "旧全局模板\n")
|
|
34
|
+
output, = capture_io { Appship::Display.upload_result({}, file: "Demo.ipa") }
|
|
35
|
+
|
|
36
|
+
assert_equal File.read(File.expand_path("../lib/appship/default_message", __dir__)), Appship::Message.template
|
|
37
|
+
assert_includes output, "应用名称: Demo\n"
|
|
38
|
+
assert_equal "旧全局模板\n", File.read(legacy_path)
|
|
39
|
+
refute File.exist?(File.join(@home, ".appship", ".default_message"))
|
|
40
|
+
refute File.exist?(@message_path)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_rejects_directory_at_message_path_without_changing_permissions
|
|
44
|
+
FileUtils.mkdir_p(@message_path, mode: 0o700)
|
|
45
|
+
original_mode = File.stat(@message_path).mode
|
|
46
|
+
|
|
47
|
+
error = assert_raises(Appship::ConfigurationError) { Appship::Message.template }
|
|
48
|
+
|
|
49
|
+
assert_includes error.message, "消息模板必须是普通文件"
|
|
50
|
+
assert_equal original_mode, File.stat(@message_path).mode
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_rejects_symlink_without_changing_target
|
|
54
|
+
target = File.join(@home, "other-file")
|
|
55
|
+
File.write(target, "keep", perm: 0o600)
|
|
56
|
+
FileUtils.mkdir_p(File.dirname(@message_path))
|
|
57
|
+
File.symlink(target, @message_path)
|
|
58
|
+
|
|
59
|
+
assert_raises(Appship::ConfigurationError) { Appship::Message.template }
|
|
60
|
+
|
|
61
|
+
assert_equal "keep", File.read(target)
|
|
62
|
+
assert_equal 0o600, File.stat(target).mode & 0o777
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_pgyer_output_renders_template_with_upload_data
|
|
66
|
+
result = { "data" => {
|
|
67
|
+
"buildName" => "Demo",
|
|
68
|
+
"buildType" => "1",
|
|
69
|
+
"buildVersion" => "1.2.3",
|
|
70
|
+
"buildBuildVersion" => "42",
|
|
71
|
+
"buildFileSize" => "1572864",
|
|
72
|
+
"buildUpdated" => "2026-10-26 10:00:00",
|
|
73
|
+
"buildKey" => "demo"
|
|
74
|
+
} }
|
|
75
|
+
options = { configuration: "Release", interactive_content: "完成 100% %{url}", password: "install-secret" }
|
|
76
|
+
|
|
77
|
+
output, = capture_io { Appship::Display.upload_result(result, file: "Demo.ipa", options: options) }
|
|
78
|
+
|
|
79
|
+
assert_includes output, "应用名称: Demo\n应用类型: iOS\n版本信息: 1.2.3(42)\n"
|
|
80
|
+
assert_includes output, "构建模式: Release\n应用大小: 1.5MB\n更新时间: 2026-10-26 10:00:00\n"
|
|
81
|
+
assert_includes output, "更新描述: 完成 100% %{url}\n安装密码: install-secret\n下载链接: https://www.pgyer.com/demo\n"
|
|
82
|
+
refute_includes output, "分发平台:"
|
|
83
|
+
refute_includes Appship::Message.template, "install-secret"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_pgyer_output_omits_missing_optional_fields
|
|
87
|
+
output, = capture_io do
|
|
88
|
+
Appship::Display.upload_result({ "buildUpdated" => "" }, file: "Demo.apk")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
assert_includes output, "应用名称: Demo\n应用类型: Android\n构建模式: 未知\n"
|
|
92
|
+
assert_includes output, "安装密码: 未设置\n下载链接: https://www.pgyer.com/\n"
|
|
93
|
+
refute_match(/版本信息:|应用大小:|更新时间:|分发平台:/, output)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_fir_output_preserves_password_status
|
|
97
|
+
result = {
|
|
98
|
+
"provider" => "fir",
|
|
99
|
+
"data" => { "name" => "Demo", "type" => "ios", "version" => "1.2.3", "build" => "42", "password_protected" => true },
|
|
100
|
+
"url" => "https://fir.im/demo"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
output, = capture_io do
|
|
104
|
+
Appship::Display.upload_result(result, file: "Demo.ipa", options: { description: "首次发布", fir_password: "install-secret" })
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
assert_includes output, "应用名称: Demo\n应用类型: iOS\n版本信息: 1.2.3(42)\n"
|
|
108
|
+
assert_includes output, "更新描述: 首次发布\n安装密码: 已设置\n下载链接: https://fir.im/demo\n"
|
|
109
|
+
refute_match(/分发平台:|构建模式:|应用大小:|更新时间:|install-secret/, output)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_fir_output_handles_missing_version_and_no_password
|
|
113
|
+
result = { "provider" => "fir", "data" => { "type" => "android" }, "url" => "https://fir.im/demo" }
|
|
114
|
+
|
|
115
|
+
output, = capture_io { Appship::Display.upload_result(result, file: "Demo.apk") }
|
|
116
|
+
|
|
117
|
+
assert_includes output, "应用名称: Demo\n应用类型: Android\n更新描述: \n安装密码: 未设置\n"
|
|
118
|
+
refute_includes output, "版本信息:"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require_relative "../lib/appship"
|
|
5
|
+
|
|
6
|
+
class MessageTest < Minitest::Test
|
|
7
|
+
def setup
|
|
8
|
+
@home = Dir.mktmpdir("appship-messages-")
|
|
9
|
+
@original_home = ENV["HOME"]
|
|
10
|
+
@original_env_file = ENV["APPSHIP_ENV_FILE"]
|
|
11
|
+
@original_directory = Dir.pwd
|
|
12
|
+
ENV["HOME"] = @home
|
|
13
|
+
ENV["APPSHIP_ENV_FILE"] = File.join(@home, "global.config")
|
|
14
|
+
@project_dir = File.join(@home, "project")
|
|
15
|
+
FileUtils.mkdir_p(File.join(@project_dir, "Demo.xcodeproj"))
|
|
16
|
+
Dir.chdir(@project_dir)
|
|
17
|
+
@path = Appship::Message.path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def teardown
|
|
21
|
+
Dir.chdir(@original_directory)
|
|
22
|
+
ENV["HOME"] = @original_home
|
|
23
|
+
ENV["APPSHIP_ENV_FILE"] = @original_env_file
|
|
24
|
+
FileUtils.remove_entry(@home)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_init_creates_only_project_samples_without_global_files
|
|
28
|
+
output, = capture_io { Appship::CLI.new.start(["init"]) }
|
|
29
|
+
|
|
30
|
+
assert_equal %w[config.yml.sample message.yml.sample], Dir.children("appship").sort
|
|
31
|
+
config_sample = File.read("appship/config.yml.sample")
|
|
32
|
+
assert_equal({ "project_name" => "Demo" }, YAML.safe_load(config_sample))
|
|
33
|
+
assert_includes config_sample, "# app_name: app-name"
|
|
34
|
+
assert_includes config_sample, "# 机器人配置"
|
|
35
|
+
%w[pgyer_api_key pgyer_password fir_api_key fir_password wechat_webhook dingtalk_webhook feishu_webhook].each do |key|
|
|
36
|
+
assert_match(/^# #{key}: "/, config_sample)
|
|
37
|
+
end
|
|
38
|
+
assert_match(/^# app_icon_badge: true/, config_sample)
|
|
39
|
+
assert_match(/^# app_icon_badge_environment: true/, config_sample)
|
|
40
|
+
assert_match(/^# app_icon_badge_version: false/, config_sample)
|
|
41
|
+
assert_equal 0o600, File.stat("#{Appship::ProjectConfig.path}.sample").mode & 0o777
|
|
42
|
+
assert_equal 0o600, File.stat("#{@path}.sample").mode & 0o777
|
|
43
|
+
message_sample = File.read("#{@path}.sample")
|
|
44
|
+
assert_includes message_sample, "可用变量"
|
|
45
|
+
assert_nil YAML.safe_load(message_sample)
|
|
46
|
+
%w[wechat dingtalk feishu].product(%w[success failure]).each do |platform, status|
|
|
47
|
+
assert_match(/^# #{platform}_webhook_#{status}_content: ""/, message_sample)
|
|
48
|
+
end
|
|
49
|
+
cli_example = message_sample.lines.drop_while { |line| line != "# cli_content: |\n" }
|
|
50
|
+
.take_while { |line| line.start_with?("# ") }.map { |line| line.delete_prefix("# ") }.join
|
|
51
|
+
assert_includes YAML.safe_load(cli_example).fetch("cli_content"), "发布成功\n版本:"
|
|
52
|
+
assert_includes output, "#{@path}.sample"
|
|
53
|
+
refute_includes Appship::Message.template, "可用变量"
|
|
54
|
+
refute File.exist?(File.join(@home, ".appship"))
|
|
55
|
+
refute File.exist?(".appship.yml")
|
|
56
|
+
refute File.exist?(@path)
|
|
57
|
+
refute File.exist?(Appship::ProjectConfig.path)
|
|
58
|
+
File.write(@path, message_sample)
|
|
59
|
+
assert_equal File.read(Appship::Message.default_path), Appship::Message.template
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_init_preserves_samples_and_active_files
|
|
63
|
+
capture_io { Appship::CLI.new.start(["init"]) }
|
|
64
|
+
config = "{\"project_name\":\"Demo\",\"app_name\":\"Custom\"}\n"
|
|
65
|
+
File.write(Appship::ProjectConfig.path, config)
|
|
66
|
+
File.write(@path, "自定义 %{name}\n")
|
|
67
|
+
File.write("#{Appship::ProjectConfig.path}.sample", config)
|
|
68
|
+
File.write("#{@path}.sample", "自定义示例 %{name}\n")
|
|
69
|
+
|
|
70
|
+
capture_io { 2.times { Appship::CLI.new.start(["init"]) } }
|
|
71
|
+
|
|
72
|
+
assert_equal config, File.read(Appship::ProjectConfig.path)
|
|
73
|
+
assert_equal "自定义 %{name}\n", File.read(@path)
|
|
74
|
+
assert_equal config, File.read("#{Appship::ProjectConfig.path}.sample")
|
|
75
|
+
assert_equal "自定义示例 %{name}\n", File.read("#{@path}.sample")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_init_can_target_another_project_directory
|
|
79
|
+
other = File.join(@home, "other")
|
|
80
|
+
FileUtils.mkdir_p(File.join(other, "Other.xcodeproj"))
|
|
81
|
+
|
|
82
|
+
capture_io { Appship::CLI.new.start(["init", "--project-dir", other]) }
|
|
83
|
+
|
|
84
|
+
assert_equal "Other", YAML.safe_load_file("#{Appship::ProjectConfig.path(other)}.sample")["project_name"]
|
|
85
|
+
assert File.file?("#{Appship::Message.path(other)}.sample")
|
|
86
|
+
refute File.exist?(@path)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_custom_template_preserves_literal_percent_and_body_comments
|
|
90
|
+
Appship::Message.init!
|
|
91
|
+
File.write(File.join(File.dirname(@path), "message"), "/* 备注 */\n%{name} 100%\n/* 正文 */\n%{description}\n")
|
|
92
|
+
|
|
93
|
+
assert_equal "Demo 100%\n/* 正文 */\n更新 %{url}\n", Appship::Message.render(name: "Demo", description: "更新 %{url}")
|
|
94
|
+
File.write(@path, YAML.dump("cli_content" => "YAML %{name}\n"))
|
|
95
|
+
assert_equal "YAML Demo\n", Appship::Message.render(name: "Demo")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_invalid_variables_are_reported_before_building_or_uploading
|
|
99
|
+
Appship::Message.init!
|
|
100
|
+
File.write(@path, YAML.dump("feishu_webhook_content" => "未知 %{unknown}\n"))
|
|
101
|
+
|
|
102
|
+
[["build", "--upload"], ["package", "--app", "missing.app", "--upload"], ["upload", "missing.ipa"]].each do |args|
|
|
103
|
+
error = assert_raises(Appship::ConfigurationError) { Appship::CLI.new.start(args) }
|
|
104
|
+
assert_includes error.message, "未知变量 unknown"
|
|
105
|
+
assert_includes error.message, @path
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_invalid_template_syntax_is_rejected
|
|
110
|
+
Appship::Message.init!
|
|
111
|
+
["cli_content: [", "cli_content: true", "cli_content: '应用 %{name'",
|
|
112
|
+
"wechat_webhook_format: image", "feishu_webhook_format: interactive", "unknown: text"].each do |content|
|
|
113
|
+
File.write(@path, content)
|
|
114
|
+
assert_raises(Appship::ConfigurationError) { Appship::Message.template }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_messages_are_isolated_by_project_directory
|
|
119
|
+
Appship::Message.init!
|
|
120
|
+
File.write(@path, YAML.dump("cli_content" => "第一个 %{name}\n"))
|
|
121
|
+
other = File.join(@home, "other")
|
|
122
|
+
Appship::Message.init!(project_dir: other)
|
|
123
|
+
File.write(Appship::Message.path(other), YAML.dump("cli_content" => "第二个 %{name}\n"))
|
|
124
|
+
|
|
125
|
+
assert_equal "第一个 Demo\n", Appship::Message.render(name: "Demo")
|
|
126
|
+
Dir.chdir(other) { assert_equal "第二个 Demo\n", Appship::Message.render(name: "Demo") }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def test_entire_message_command_group_is_removed
|
|
130
|
+
[[], ["init"], ["-e"], ["-o", "file.txt"]].each do |args|
|
|
131
|
+
error = assert_raises(Appship::ConfigurationError) { Appship::CLI.new.start(["message", *args]) }
|
|
132
|
+
assert_includes error.message, "未知命令: message"
|
|
133
|
+
end
|
|
134
|
+
output, = capture_io { Appship::CLI.new.start(["--help"]) }
|
|
135
|
+
refute_includes output, "appship message"
|
|
136
|
+
refute File.exist?(@path)
|
|
137
|
+
end
|
|
138
|
+
end
|