2019-03-11 16:56:48 +00:00
|
|
|
package namecheap
|
2016-03-14 21:28:40 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
|
|
|
"net/url"
|
|
|
|
"testing"
|
2018-06-21 17:06:16 +00:00
|
|
|
"time"
|
2018-09-15 17:07:24 +00:00
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/stretchr/testify/require"
|
2016-03-14 21:28:40 +00:00
|
|
|
)
|
|
|
|
|
2018-12-06 21:50:17 +00:00
|
|
|
const (
|
2018-10-12 17:29:18 +00:00
|
|
|
envTestUser = "foo"
|
|
|
|
envTestKey = "bar"
|
|
|
|
envTestClientIP = "10.0.0.1"
|
2016-03-14 21:28:40 +00:00
|
|
|
)
|
|
|
|
|
2018-10-12 17:29:18 +00:00
|
|
|
func TestDNSProvider_getHosts(t *testing.T) {
|
2018-10-09 16:16:05 +00:00
|
|
|
for _, test := range testCases {
|
2018-09-15 17:07:24 +00:00
|
|
|
t.Run(test.name, func(t *testing.T) {
|
2021-11-01 23:52:38 +00:00
|
|
|
p := setupTest(t, &test)
|
2018-09-15 17:07:24 +00:00
|
|
|
|
2020-01-18 01:25:50 +00:00
|
|
|
ch, err := newChallenge(test.domain, "")
|
2018-10-09 16:16:05 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2021-11-01 23:52:38 +00:00
|
|
|
hosts, err := p.getHosts(ch.sld, ch.tld)
|
2018-09-15 17:07:24 +00:00
|
|
|
if test.errString != "" {
|
|
|
|
assert.EqualError(t, err, test.errString)
|
|
|
|
} else {
|
|
|
|
assert.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
next1:
|
|
|
|
for _, h := range hosts {
|
|
|
|
for _, th := range test.hosts {
|
|
|
|
if h == th {
|
|
|
|
continue next1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
t.Errorf("getHosts case %s unexpected record [%s:%s:%s]", test.name, h.Type, h.Name, h.Address)
|
|
|
|
}
|
|
|
|
|
|
|
|
next2:
|
|
|
|
for _, th := range test.hosts {
|
|
|
|
for _, h := range hosts {
|
|
|
|
if h == th {
|
|
|
|
continue next2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
t.Errorf("getHosts case %s missing record [%s:%s:%s]", test.name, th.Type, th.Name, th.Address)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-12 17:29:18 +00:00
|
|
|
func TestDNSProvider_setHosts(t *testing.T) {
|
2018-10-09 16:16:05 +00:00
|
|
|
for _, test := range testCases {
|
2018-09-15 17:07:24 +00:00
|
|
|
t.Run(test.name, func(t *testing.T) {
|
2021-11-01 23:52:38 +00:00
|
|
|
p := setupTest(t, &test)
|
2018-10-09 16:16:05 +00:00
|
|
|
|
2020-01-18 01:25:50 +00:00
|
|
|
ch, err := newChallenge(test.domain, "")
|
2018-10-09 16:16:05 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2021-11-01 23:52:38 +00:00
|
|
|
hosts, err := p.getHosts(ch.sld, ch.tld)
|
2018-09-15 17:07:24 +00:00
|
|
|
if test.errString != "" {
|
|
|
|
assert.EqualError(t, err, test.errString)
|
|
|
|
} else {
|
2018-09-24 19:07:20 +00:00
|
|
|
require.NoError(t, err)
|
2018-09-15 17:07:24 +00:00
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-11-01 23:52:38 +00:00
|
|
|
err = p.setHosts(ch.sld, ch.tld, hosts)
|
2018-09-24 19:07:20 +00:00
|
|
|
require.NoError(t, err)
|
2018-09-15 17:07:24 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-12 17:29:18 +00:00
|
|
|
func TestDNSProvider_Present(t *testing.T) {
|
2018-10-09 16:16:05 +00:00
|
|
|
for _, test := range testCases {
|
2018-09-15 17:07:24 +00:00
|
|
|
t.Run(test.name, func(t *testing.T) {
|
2021-11-01 23:52:38 +00:00
|
|
|
p := setupTest(t, &test)
|
2018-09-15 17:07:24 +00:00
|
|
|
|
2021-11-01 23:52:38 +00:00
|
|
|
err := p.Present(test.domain, "", "dummyKey")
|
2018-09-15 17:07:24 +00:00
|
|
|
if test.errString != "" {
|
|
|
|
assert.EqualError(t, err, "namecheap: "+test.errString)
|
|
|
|
} else {
|
|
|
|
assert.NoError(t, err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-12 17:29:18 +00:00
|
|
|
func TestDNSProvider_CleanUp(t *testing.T) {
|
2018-10-09 16:16:05 +00:00
|
|
|
for _, test := range testCases {
|
2018-09-15 17:07:24 +00:00
|
|
|
t.Run(test.name, func(t *testing.T) {
|
2021-11-01 23:52:38 +00:00
|
|
|
p := setupTest(t, &test)
|
2018-09-15 17:07:24 +00:00
|
|
|
|
2021-11-01 23:52:38 +00:00
|
|
|
err := p.CleanUp(test.domain, "", "dummyKey")
|
2018-09-15 17:07:24 +00:00
|
|
|
if test.errString != "" {
|
|
|
|
assert.EqualError(t, err, "namecheap: "+test.errString)
|
|
|
|
} else {
|
|
|
|
assert.NoError(t, err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-09 16:16:05 +00:00
|
|
|
func TestDomainSplit(t *testing.T) {
|
2018-09-15 17:07:24 +00:00
|
|
|
tests := []struct {
|
|
|
|
domain string
|
|
|
|
valid bool
|
|
|
|
tld string
|
|
|
|
sld string
|
|
|
|
host string
|
|
|
|
}{
|
|
|
|
{domain: "a.b.c.test.co.uk", valid: true, tld: "co.uk", sld: "test", host: "a.b.c"},
|
|
|
|
{domain: "test.co.uk", valid: true, tld: "co.uk", sld: "test"},
|
|
|
|
{domain: "test.com", valid: true, tld: "com", sld: "test"},
|
|
|
|
{domain: "test.co.com", valid: true, tld: "co.com", sld: "test"},
|
|
|
|
{domain: "www.test.com.au", valid: true, tld: "com.au", sld: "test", host: "www"},
|
|
|
|
{domain: "www.za.com", valid: true, tld: "za.com", sld: "www"},
|
2020-01-18 01:25:50 +00:00
|
|
|
{domain: "my.test.tf", valid: true, tld: "tf", sld: "test", host: "my"},
|
2018-09-15 17:07:24 +00:00
|
|
|
{},
|
|
|
|
{domain: "a"},
|
|
|
|
{domain: "com"},
|
2020-01-18 01:25:50 +00:00
|
|
|
{domain: "com.au"},
|
2018-09-15 17:07:24 +00:00
|
|
|
{domain: "co.com"},
|
|
|
|
{domain: "co.uk"},
|
2020-01-18 01:25:50 +00:00
|
|
|
{domain: "tf"},
|
2018-09-15 17:07:24 +00:00
|
|
|
{domain: "za.com"},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, test := range tests {
|
|
|
|
test := test
|
|
|
|
t.Run(test.domain, func(t *testing.T) {
|
|
|
|
valid := true
|
2020-01-18 01:25:50 +00:00
|
|
|
ch, err := newChallenge(test.domain, "")
|
2018-09-15 17:07:24 +00:00
|
|
|
if err != nil {
|
|
|
|
valid = false
|
|
|
|
}
|
|
|
|
|
|
|
|
if test.valid && !valid {
|
|
|
|
t.Errorf("Expected '%s' to split", test.domain)
|
|
|
|
} else if !test.valid && valid {
|
|
|
|
t.Errorf("Expected '%s' to produce error", test.domain)
|
|
|
|
}
|
|
|
|
|
|
|
|
if test.valid && valid {
|
2020-01-18 01:25:50 +00:00
|
|
|
require.NotNil(t, ch)
|
|
|
|
assert.Equal(t, test.domain, ch.domain, "domain")
|
|
|
|
assert.Equal(t, test.tld, ch.tld, "tld")
|
|
|
|
assert.Equal(t, test.sld, ch.sld, "sld")
|
|
|
|
assert.Equal(t, test.host, ch.host, "host")
|
2018-09-15 17:07:24 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-28 22:39:00 +00:00
|
|
|
func assertHdr(t *testing.T, tc *testCase, values *url.Values) {
|
2020-01-18 01:25:50 +00:00
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
ch, _ := newChallenge(tc.domain, "")
|
|
|
|
assert.Equal(t, envTestUser, values.Get("ApiUser"), "ApiUser")
|
|
|
|
assert.Equal(t, envTestKey, values.Get("ApiKey"), "ApiKey")
|
|
|
|
assert.Equal(t, envTestUser, values.Get("UserName"), "UserName")
|
|
|
|
assert.Equal(t, envTestClientIP, values.Get("ClientIp"), "ClientIp")
|
|
|
|
assert.Equal(t, ch.sld, values.Get("SLD"), "SLD")
|
|
|
|
assert.Equal(t, ch.tld, values.Get("TLD"), "TLD")
|
2016-03-14 21:28:40 +00:00
|
|
|
}
|
|
|
|
|
2021-11-01 23:52:38 +00:00
|
|
|
func setupTest(t *testing.T, tc *testCase) *DNSProvider {
|
2020-12-28 22:39:00 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2021-11-01 23:52:38 +00:00
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2018-10-12 17:29:18 +00:00
|
|
|
switch r.Method {
|
|
|
|
case http.MethodGet:
|
|
|
|
values := r.URL.Query()
|
|
|
|
cmd := values.Get("Command")
|
|
|
|
switch cmd {
|
|
|
|
case "namecheap.domains.dns.getHosts":
|
2020-12-28 22:39:00 +00:00
|
|
|
assertHdr(t, tc, &values)
|
2018-10-12 17:29:18 +00:00
|
|
|
w.WriteHeader(http.StatusOK)
|
2019-01-24 20:40:44 +00:00
|
|
|
fmt.Fprint(w, tc.getHostsResponse)
|
2018-10-12 17:29:18 +00:00
|
|
|
default:
|
|
|
|
t.Errorf("Unexpected GET command: %s", cmd)
|
|
|
|
}
|
|
|
|
|
|
|
|
case http.MethodPost:
|
|
|
|
err := r.ParseForm()
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
values := r.Form
|
|
|
|
cmd := values.Get("Command")
|
|
|
|
switch cmd {
|
|
|
|
case "namecheap.domains.dns.setHosts":
|
2020-12-28 22:39:00 +00:00
|
|
|
assertHdr(t, tc, &values)
|
2018-10-12 17:29:18 +00:00
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
fmt.Fprint(w, tc.setHostsResponse)
|
|
|
|
default:
|
|
|
|
t.Errorf("Unexpected POST command: %s", cmd)
|
|
|
|
}
|
2016-03-14 21:28:40 +00:00
|
|
|
|
|
|
|
default:
|
2018-10-12 17:29:18 +00:00
|
|
|
t.Errorf("Unexpected http method: %s", r.Method)
|
2016-03-14 21:28:40 +00:00
|
|
|
}
|
2021-11-01 23:52:38 +00:00
|
|
|
})
|
2020-12-28 22:39:00 +00:00
|
|
|
|
2021-11-01 23:52:38 +00:00
|
|
|
server := httptest.NewServer(handler)
|
2020-12-28 22:39:00 +00:00
|
|
|
t.Cleanup(server.Close)
|
|
|
|
|
2021-11-01 23:52:38 +00:00
|
|
|
return mockDNSProvider(t, server.URL)
|
2016-03-14 21:28:40 +00:00
|
|
|
}
|
|
|
|
|
2021-11-01 23:52:38 +00:00
|
|
|
func mockDNSProvider(t *testing.T, baseURL string) *DNSProvider {
|
|
|
|
t.Helper()
|
|
|
|
|
2018-09-15 17:07:24 +00:00
|
|
|
config := NewDefaultConfig()
|
2021-03-04 19:16:59 +00:00
|
|
|
config.BaseURL = baseURL
|
2018-10-12 17:29:18 +00:00
|
|
|
config.APIUser = envTestUser
|
|
|
|
config.APIKey = envTestKey
|
|
|
|
config.ClientIP = envTestClientIP
|
2018-09-15 17:07:24 +00:00
|
|
|
config.HTTPClient = &http.Client{Timeout: 60 * time.Second}
|
|
|
|
|
2018-10-09 16:16:05 +00:00
|
|
|
provider, err := NewDNSProviderConfig(config)
|
2021-11-01 23:52:38 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2018-09-15 17:07:24 +00:00
|
|
|
return provider
|
2016-03-14 21:28:40 +00:00
|
|
|
}
|
|
|
|
|
2018-10-09 16:16:05 +00:00
|
|
|
type testCase struct {
|
2016-03-14 21:28:40 +00:00
|
|
|
name string
|
|
|
|
domain string
|
2018-10-09 16:16:05 +00:00
|
|
|
hosts []Record
|
2016-03-14 21:28:40 +00:00
|
|
|
errString string
|
|
|
|
getHostsResponse string
|
|
|
|
setHostsResponse string
|
|
|
|
}
|
|
|
|
|
2018-10-09 16:16:05 +00:00
|
|
|
var testCases = []testCase{
|
2016-03-14 21:28:40 +00:00
|
|
|
{
|
2018-09-15 17:07:24 +00:00
|
|
|
name: "Test:Success:1",
|
|
|
|
domain: "test.example.com",
|
2018-10-09 16:16:05 +00:00
|
|
|
hosts: []Record{
|
2018-09-15 17:07:24 +00:00
|
|
|
{Type: "A", Name: "home", Address: "10.0.0.1", MXPref: "10", TTL: "1799"},
|
|
|
|
{Type: "A", Name: "www", Address: "10.0.0.2", MXPref: "10", TTL: "1200"},
|
|
|
|
{Type: "AAAA", Name: "a", Address: "::0", MXPref: "10", TTL: "1799"},
|
|
|
|
{Type: "CNAME", Name: "*", Address: "example.com.", MXPref: "10", TTL: "1799"},
|
|
|
|
{Type: "MXE", Name: "example.com", Address: "10.0.0.5", MXPref: "10", TTL: "1800"},
|
|
|
|
{Type: "URL", Name: "xyz", Address: "https://google.com", MXPref: "10", TTL: "1799"},
|
2016-03-14 21:28:40 +00:00
|
|
|
},
|
2018-09-15 17:07:24 +00:00
|
|
|
getHostsResponse: responseGetHostsSuccess1,
|
|
|
|
setHostsResponse: responseSetHostsSuccess1,
|
2016-03-14 21:28:40 +00:00
|
|
|
},
|
|
|
|
{
|
2018-09-15 17:07:24 +00:00
|
|
|
name: "Test:Success:2",
|
|
|
|
domain: "example.com",
|
2018-10-09 16:16:05 +00:00
|
|
|
hosts: []Record{
|
2018-09-15 17:07:24 +00:00
|
|
|
{Type: "A", Name: "@", Address: "10.0.0.2", MXPref: "10", TTL: "1200"},
|
|
|
|
{Type: "A", Name: "www", Address: "10.0.0.3", MXPref: "10", TTL: "60"},
|
2016-03-14 21:28:40 +00:00
|
|
|
},
|
2018-09-15 17:07:24 +00:00
|
|
|
getHostsResponse: responseGetHostsSuccess2,
|
|
|
|
setHostsResponse: responseSetHostsSuccess2,
|
2016-03-14 21:28:40 +00:00
|
|
|
},
|
|
|
|
{
|
2018-09-15 17:07:24 +00:00
|
|
|
name: "Test:Error:BadApiKey:1",
|
|
|
|
domain: "test.example.com",
|
|
|
|
errString: "API Key is invalid or API access has not been enabled [1011102]",
|
|
|
|
getHostsResponse: responseGetHostsErrorBadAPIKey1,
|
2016-03-14 21:28:40 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2018-12-06 21:50:17 +00:00
|
|
|
const responseGetHostsSuccess1 = `<?xml version="1.0" encoding="utf-8"?>
|
2016-03-14 21:28:40 +00:00
|
|
|
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
|
|
|
|
<Errors />
|
|
|
|
<Warnings />
|
|
|
|
<RequestedCommand>namecheap.domains.dns.getHosts</RequestedCommand>
|
|
|
|
<CommandResponse Type="namecheap.domains.dns.getHosts">
|
|
|
|
<DomainDNSGetHostsResult Domain="example.com" EmailType="MXE" IsUsingOurDNS="true">
|
|
|
|
<host HostId="217076" Name="www" Type="A" Address="10.0.0.2" MXPref="10" TTL="1200" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
|
|
|
|
<host HostId="217069" Name="home" Type="A" Address="10.0.0.1" MXPref="10" TTL="1799" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
|
|
|
|
<host HostId="217071" Name="a" Type="AAAA" Address="::0" MXPref="10" TTL="1799" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
|
|
|
|
<host HostId="217075" Name="*" Type="CNAME" Address="example.com." MXPref="10" TTL="1799" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
|
|
|
|
<host HostId="217073" Name="example.com" Type="MXE" Address="10.0.0.5" MXPref="10" TTL="1800" AssociatedAppTitle="MXE" FriendlyName="MXE1" IsActive="true" IsDDNSEnabled="false" />
|
|
|
|
<host HostId="217077" Name="xyz" Type="URL" Address="https://google.com" MXPref="10" TTL="1799" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
|
|
|
|
</DomainDNSGetHostsResult>
|
|
|
|
</CommandResponse>
|
|
|
|
<Server>PHX01SBAPI01</Server>
|
|
|
|
<GMTTimeDifference>--5:00</GMTTimeDifference>
|
|
|
|
<ExecutionTime>3.338</ExecutionTime>
|
|
|
|
</ApiResponse>`
|
|
|
|
|
2018-12-06 21:50:17 +00:00
|
|
|
const responseSetHostsSuccess1 = `<?xml version="1.0" encoding="utf-8"?>
|
2016-03-14 21:28:40 +00:00
|
|
|
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
|
|
|
|
<Errors />
|
|
|
|
<Warnings />
|
|
|
|
<RequestedCommand>namecheap.domains.dns.setHosts</RequestedCommand>
|
|
|
|
<CommandResponse Type="namecheap.domains.dns.setHosts">
|
|
|
|
<DomainDNSSetHostsResult Domain="example.com" IsSuccess="true">
|
|
|
|
<Warnings />
|
|
|
|
</DomainDNSSetHostsResult>
|
|
|
|
</CommandResponse>
|
|
|
|
<Server>PHX01SBAPI01</Server>
|
|
|
|
<GMTTimeDifference>--5:00</GMTTimeDifference>
|
|
|
|
<ExecutionTime>2.347</ExecutionTime>
|
|
|
|
</ApiResponse>`
|
|
|
|
|
2018-12-06 21:50:17 +00:00
|
|
|
const responseGetHostsSuccess2 = `<?xml version="1.0" encoding="utf-8"?>
|
2016-03-14 21:28:40 +00:00
|
|
|
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
|
|
|
|
<Errors />
|
|
|
|
<Warnings />
|
|
|
|
<RequestedCommand>namecheap.domains.dns.getHosts</RequestedCommand>
|
|
|
|
<CommandResponse Type="namecheap.domains.dns.getHosts">
|
|
|
|
<DomainDNSGetHostsResult Domain="example.com" EmailType="MXE" IsUsingOurDNS="true">
|
|
|
|
<host HostId="217076" Name="@" Type="A" Address="10.0.0.2" MXPref="10" TTL="1200" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
|
|
|
|
<host HostId="217069" Name="www" Type="A" Address="10.0.0.3" MXPref="10" TTL="60" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
|
|
|
|
</DomainDNSGetHostsResult>
|
|
|
|
</CommandResponse>
|
|
|
|
<Server>PHX01SBAPI01</Server>
|
|
|
|
<GMTTimeDifference>--5:00</GMTTimeDifference>
|
|
|
|
<ExecutionTime>3.338</ExecutionTime>
|
|
|
|
</ApiResponse>`
|
|
|
|
|
2018-12-06 21:50:17 +00:00
|
|
|
const responseSetHostsSuccess2 = `<?xml version="1.0" encoding="utf-8"?>
|
2016-03-14 21:28:40 +00:00
|
|
|
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
|
|
|
|
<Errors />
|
|
|
|
<Warnings />
|
|
|
|
<RequestedCommand>namecheap.domains.dns.setHosts</RequestedCommand>
|
|
|
|
<CommandResponse Type="namecheap.domains.dns.setHosts">
|
|
|
|
<DomainDNSSetHostsResult Domain="example.com" IsSuccess="true">
|
|
|
|
<Warnings />
|
|
|
|
</DomainDNSSetHostsResult>
|
|
|
|
</CommandResponse>
|
|
|
|
<Server>PHX01SBAPI01</Server>
|
|
|
|
<GMTTimeDifference>--5:00</GMTTimeDifference>
|
|
|
|
<ExecutionTime>2.347</ExecutionTime>
|
|
|
|
</ApiResponse>`
|
|
|
|
|
2018-12-06 21:50:17 +00:00
|
|
|
const responseGetHostsErrorBadAPIKey1 = `<?xml version="1.0" encoding="utf-8"?>
|
2016-03-14 21:28:40 +00:00
|
|
|
<ApiResponse Status="ERROR" xmlns="http://api.namecheap.com/xml.response">
|
|
|
|
<Errors>
|
|
|
|
<Error Number="1011102">API Key is invalid or API access has not been enabled</Error>
|
|
|
|
</Errors>
|
|
|
|
<Warnings />
|
|
|
|
<RequestedCommand />
|
|
|
|
<Server>PHX01SBAPI01</Server>
|
|
|
|
<GMTTimeDifference>--5:00</GMTTimeDifference>
|
|
|
|
<ExecutionTime>0</ExecutionTime>
|
|
|
|
</ApiResponse>`
|