-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
First commit #1
base: main
Are you sure you want to change the base?
First commit #1
Changes from all commits
e6e6234
fe580b9
3ea8fc4
73f288a
e521c63
5f0cbaa
2eedbf2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,2 +1,13 @@ | ||||||
# appcircle-testinium-upload-app-component | ||||||
Appcircle Testinium App Upload component allows you to upload mobile applications from Appcircle to the Testinium platform, making it easy to run automated tests as part of your CI/CD pipeline. | ||||||
# Appcircle _Testinium Upload App_ component | ||||||
|
||||||
The **Testinium Upload App** component enables uploading mobile applications to the [Testinium](https://testinium.com/) platform for automated testing directly from Appcircle. This step serves as a prerequisite for executing test plans, enabling efficient and automated testing directly within the Appcircle environment. | ||||||
|
||||||
## Required Inputs | ||||||
|
||||||
- `AC_TESTINIUM_APP_PATH`: Full path of the build. For example $AC_EXPORT_DIR/Myapp.ipa. | ||||||
- `AC_TESTINIUM_USERNAME`: Testinium username. | ||||||
- `AC_TESTINIUM_PASSWORD`: Testinium password. | ||||||
- `AC_TESTINIUM_PROJECT_ID`: Testinium project ID. | ||||||
- `AC_TESTINIUM_COMPANY_ID`: Testinium company ID. | ||||||
- `AC_TESTINIUM_TIMEOUT`: Testinium plan timeout in minutes. | ||||||
- `AC_TESTINIUM_MAX_API_RETRY_COUNT`: Determine max repetition in case of Testinium platform congestion or API errors. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I searched briefly, the word retry is used more often in the sense of repeating a connection again. The word repetition is used in the sense of repeating something as independently error. Example usages
Suggested change
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,51 @@ | ||||||
platform: Common | ||||||
buildPlatform: | ||||||
displayName: Testinium Upload App | ||||||
description: "Enables uploading mobile applications to the Testinium platform." | ||||||
inputs: | ||||||
- key: "AC_TESTINIUM_APP_PATH" | ||||||
defaultValue: "$AC_TESTINIUM_APP_PATH" | ||||||
isRequired: true | ||||||
title: Path of the build | ||||||
description: "Full path of the build file. Example for iOS: `$AC_EXPORT_DIR/MyApp.ipa`. Example for Android: `$AC_APK_PATH`." | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same suggestion;
Suggested change
|
||||||
helpText: | ||||||
- key: "AC_TESTINIUM_USERNAME" | ||||||
defaultValue: "$AC_TESTINIUM_USERNAME" | ||||||
isRequired: true | ||||||
title: Username | ||||||
description: "Testinium username." | ||||||
helpText: | ||||||
- key: "AC_TESTINIUM_PASSWORD" | ||||||
defaultValue: "$AC_TESTINIUM_PASSWORD" | ||||||
isRequired: true | ||||||
title: Password | ||||||
description: "Testinium password." | ||||||
helpText: | ||||||
- key: "AC_TESTINIUM_PROJECT_ID" | ||||||
defaultValue: "$AC_TESTINIUM_PROJECT_ID" | ||||||
isRequired: true | ||||||
title: Project ID | ||||||
description: "Testinium project ID." | ||||||
helpText: | ||||||
- key: "AC_TESTINIUM_COMPANY_ID" | ||||||
defaultValue: "$AC_TESTINIUM_COMPANY_ID" | ||||||
isRequired: true | ||||||
title: Company ID | ||||||
description: "Testinium company ID." | ||||||
helpText: | ||||||
- key: "AC_TESTINIUM_TIMEOUT" | ||||||
defaultValue: "60" | ||||||
isRequired: true | ||||||
title: Timeout | ||||||
description: "Testinium plan timeout in minutes." | ||||||
helpText: | ||||||
- key: "AC_TESTINIUM_MAX_API_RETRY_COUNT" | ||||||
defaultValue: "4" | ||||||
isRequired: true | ||||||
title: Testinium Maximum API Repetition Count | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
description: "Determine max repetition in case of Testinium platform congestion or API errors." | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
helpText: | ||||||
processFilename: ruby | ||||||
processArguments: '%AC_STEP_TEMP%/main.rb' | ||||||
files: | ||||||
- "main.rb" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'net/http' | ||
require 'json' | ||
require 'date' | ||
require 'colored' | ||
|
||
def env_has_key(key) | ||
!ENV[key].nil? && ENV[key] != '' ? ENV[key] : abort("Missing #{key}.".red) | ||
end | ||
|
||
MINUTES_IN_A_DAY = 1440 | ||
file = env_has_key('AC_TESTINIUM_APP_PATH') | ||
$file = file | ||
$file_name = File.basename(file) | ||
$file_name_str = $file_name.to_s | ||
$extension = File.extname($file_name) | ||
$username = env_has_key('AC_TESTINIUM_USERNAME') | ||
$password = env_has_key('AC_TESTINIUM_PASSWORD') | ||
$project_id = env_has_key('AC_TESTINIUM_PROJECT_ID') | ||
$company_id = env_has_key('AC_TESTINIUM_COMPANY_ID') | ||
$each_api_max_retry_count = env_has_key('AC_TESTINIUM_MAX_API_RETRY_COUNT').to_i | ||
timeout = env_has_key('AC_TESTINIUM_TIMEOUT').to_i | ||
date_now = DateTime.now | ||
$end_time = date_now + Rational(timeout, MINUTES_IN_A_DAY) | ||
$time_period = 30 | ||
|
||
def get_parsed_response(response) | ||
JSON.parse(response, symbolize_names: true) | ||
rescue JSON::ParserError, TypeError => e | ||
puts "\nJSON expected but received: #{response}".red | ||
puts "Error Message: #{e}".red | ||
exit(1) | ||
end | ||
|
||
def check_timeout() | ||
puts "Checking timeout...".yellow | ||
now = DateTime.now | ||
|
||
if now > $end_time | ||
puts "Timeout exceeded! If you want to allow more time, please increase the AC_TESTINIUM_TIMEOUT input value.".red | ||
exit(1) | ||
end | ||
end | ||
|
||
def is_count_less_than_max_api_retry(count) | ||
return count < $each_api_max_retry_count | ||
end | ||
|
||
def login() | ||
puts "Logging in to Testinium...".yellow | ||
uri = URI.parse('https://account.testinium.com/uaa/oauth/token') | ||
token = 'dGVzdGluaXVtU3VpdGVUcnVzdGVkQ2xpZW50OnRlc3Rpbml1bVN1aXRlU2VjcmV0S2V5' | ||
count = 1 | ||
|
||
while is_count_less_than_max_api_retry(count) | ||
check_timeout() | ||
puts "Signing in. Attempt: #{count}".blue | ||
|
||
req = Net::HTTP::Post.new(uri.request_uri, { 'Content-Type' => 'application/json', 'Authorization' => "Basic #{token}" }) | ||
req.set_form_data({ 'grant_type' => 'password', 'username' => $username, 'password' => $password }) | ||
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } | ||
|
||
if res.is_a?(Net::HTTPSuccess) | ||
puts "Successfully logged in...".green | ||
return get_parsed_response(res.body)[:access_token] | ||
elsif res.is_a?(Net::HTTPUnauthorized) | ||
puts get_parsed_response(res.body)[:error_description].red | ||
count += 1 | ||
else | ||
puts "Login error: #{get_parsed_response(res.body)}".red | ||
count += 1 | ||
end | ||
end | ||
exit(1) | ||
end | ||
|
||
def find_project(access_token) | ||
count = 1 | ||
puts "Searching for project...".blue | ||
|
||
while is_count_less_than_max_api_retry(count) do | ||
check_timeout() | ||
puts "Finding project. Attempt: #{count}".yellow | ||
|
||
uri = URI.parse("https://testinium.io/Testinium.RestApi/api/projects/#{$project_id}") | ||
req = Net::HTTP::Get.new(uri.request_uri, { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{access_token}", 'current-company-id' => "#{$company_id}" }) | ||
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } | ||
|
||
if res.is_a?(Net::HTTPSuccess) | ||
puts "Project found successfully!".green | ||
return get_parsed_response(res.body) | ||
elsif res.is_a?(Net::HTTPClientError) | ||
puts get_parsed_response(res.body)[:message].red | ||
count += 1 | ||
else | ||
puts "Project search error! Server response: #{get_parsed_response(res.body)}".red | ||
count += 1 | ||
end | ||
end | ||
exit(1) | ||
end | ||
|
||
def upload(access_token) | ||
count = 1 | ||
|
||
while is_count_less_than_max_api_retry(count) do | ||
check_timeout() | ||
puts "Uploading #{$file_name} to Testinium... Attempt: #{count}".yellow | ||
|
||
uri = URI.parse('https://testinium.io/Testinium.RestApi/api/file/upload') | ||
req = Net::HTTP::Post.new(uri.request_uri, { 'Accept' => '*/*', 'Authorization' => "Bearer #{access_token}", 'current-company-id' => "#{$company_id}" }) | ||
form_data = [['file', File.open($file)], %w[isSignRequired true]] | ||
req.set_form(form_data, 'multipart/form-data') | ||
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } | ||
|
||
if res.is_a?(Net::HTTPSuccess) | ||
puts "File uploaded successfully!".green | ||
return get_parsed_response(res.body) | ||
elsif res.is_a?(Net::HTTPClientError) | ||
puts get_parsed_response(res.body)[:message].red | ||
count += 1 | ||
else | ||
puts "File upload error! Server response: #{get_parsed_response(res.body)}".red | ||
count += 1 | ||
end | ||
end | ||
exit(1) | ||
end | ||
|
||
def update_project(project, file_response, access_token) | ||
count = 1 | ||
file_token = file_response[:file_token] | ||
ios_meta = file_response[:meta_data] | ||
abort('Upload error: File token missing.'.red) if file_token.nil? | ||
|
||
puts "File uploaded successfully #{file_token}".green | ||
|
||
dict = { | ||
'enabled' => true, | ||
'test_framework' => project[:test_framework], | ||
'test_runner_tool' => project[:test_runner_tool], | ||
'repository_path' => project[:repository_path], | ||
'test_file_type' => project[:test_file_type], | ||
'project_name' => project[:project_name] | ||
} | ||
|
||
case $extension | ||
when '.ipa' | ||
puts "iOS app uploading...".blue | ||
dict[:ios_mobile_app] = $file_name_str | ||
dict[:ios_app_hash] = project[:ios_app_hash] | ||
dict[:ios_file_token] = file_token | ||
dict[:ios_meta] = ios_meta | ||
when '.apk' | ||
puts "Android app uploading...".blue | ||
dict[:android_mobile_app] = $file_name_str | ||
dict[:android_file_token] = file_token | ||
else | ||
abort 'Error: Only .apk and .ipa files are supported.'.red | ||
end | ||
|
||
while is_count_less_than_max_api_retry(count) do | ||
check_timeout() | ||
puts "Updating Testinium project... Attempt: #{count}".yellow | ||
|
||
uri = URI.parse("https://testinium.io/Testinium.RestApi/api/projects/#{project[:id]}") | ||
req = Net::HTTP::Put.new(uri.request_uri, { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{access_token}", 'current-company-id' => "#{$company_id}" }) | ||
req.body = JSON.dump(dict) | ||
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } | ||
|
||
if res.is_a?(Net::HTTPSuccess) | ||
puts "Project updated successfully!".green | ||
return get_parsed_response(res.body) | ||
elsif res.is_a?(Net::HTTPClientError) | ||
puts get_parsed_response(res.body)[:message].red | ||
count += 1 | ||
else | ||
puts "Project update error! Server response: #{get_parsed_response(res.body)}".red | ||
count += 1 | ||
end | ||
end | ||
exit(1) | ||
end | ||
|
||
access_token = login() | ||
project = find_project(access_token) | ||
file_response = upload(access_token) | ||
update_project(project, file_response, access_token) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same suggestion here: