Skip to content
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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions README.md
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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same suggestion here:

Suggested change
- `AC_TESTINIUM_APP_PATH`: Full path of the build. For example $AC_EXPORT_DIR/Myapp.ipa.
- `AC_TESTINIUM_APP_PATH`: Full path of the build. For example $AC_OUTPUT_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.

Choose a reason for hiding this comment

The 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

If the connection fails, the system will retry after 5 seconds.

Learning a new language requires constant repetition.
Suggested change
- `AC_TESTINIUM_MAX_API_RETRY_COUNT`: Determine max repetition in case of Testinium platform congestion or API errors.
- `AC_TESTINIUM_MAX_API_RETRY_COUNT`: Determine max retry in case of Testinium platform congestion or API errors.

51 changes: 51 additions & 0 deletions component.yaml
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`."

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same suggestion;

Suggested change
description: "Full path of the build file. Example for iOS: `$AC_EXPORT_DIR/MyApp.ipa`. Example for Android: `$AC_APK_PATH`."
description: "Full path of the build file. Example for iOS: `$AC_OUTPUT_DIR/MyApp.ipa`. Example for Android: `$AC_APK_PATH`."

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
title: Testinium Maximum API Repetition Count
title: Testinium Maximum API Request Retry Count

description: "Determine max repetition in case of Testinium platform congestion or API errors."

Choose a reason for hiding this comment

The 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."
description: "Determine max retry in case of Testinium platform congestion or API errors."

helpText:
processFilename: ruby
processArguments: '%AC_STEP_TEMP%/main.rb'
files:
- "main.rb"
189 changes: 189 additions & 0 deletions 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)