Repository: xenolf/lego
Pull request #703 (2018-11-05 14:09:02)

Add DNS Provider for TransIP
I've written a DNS provider for the Dutch provider TransIP (transip.nl). They've just launched a new official Go Library which this project uses. Let me know if there are any questions before merging!

mdbraber: Output of test:

mdbraber-mbp:transip mdbraber$ go test -v -cover
=== RUN   TestNewDNSProvider
=== RUN   TestNewDNSProvider/success
=== RUN   TestNewDNSProvider/missing_all_credentials
=== RUN   TestNewDNSProvider/missing_account_name
=== RUN   TestNewDNSProvider/missing_private_key_path
=== RUN   TestNewDNSProvider/could_not_open_private_key_path
--- PASS: TestNewDNSProvider (0.00s)
    --- PASS: TestNewDNSProvider/success (0.00s)
    --- PASS: TestNewDNSProvider/missing_all_credentials (0.00s)
    --- PASS: TestNewDNSProvider/missing_account_name (0.00s)
    --- PASS: TestNewDNSProvider/missing_private_key_path (0.00s)
    --- PASS: TestNewDNSProvider/could_not_open_private_key_path (0.00s)
=== RUN   TestNewDNSProviderConfig
=== RUN   TestNewDNSProviderConfig/success
=== RUN   TestNewDNSProviderConfig/missing_all_credentials
=== RUN   TestNewDNSProviderConfig/missing_account_name
=== RUN   TestNewDNSProviderConfig/missing_private_key_path
=== RUN   TestNewDNSProviderConfig/could_not_open_private_key_path
--- PASS: TestNewDNSProviderConfig (0.00s)
    --- PASS: TestNewDNSProviderConfig/success (0.00s)
    --- PASS: TestNewDNSProviderConfig/missing_all_credentials (0.00s)
    --- PASS: TestNewDNSProviderConfig/missing_account_name (0.00s)
    --- PASS: TestNewDNSProviderConfig/missing_private_key_path (0.00s)
    --- PASS: TestNewDNSProviderConfig/could_not_open_private_key_path (0.00s)
=== RUN   TestLivePresent
--- SKIP: TestLivePresent (0.00s)
    transip_test.go:140: skipping live test
=== RUN   TestLiveCleanUp
--- SKIP: TestLiveCleanUp (0.00s)
    transip_test.go:153: skipping live test
PASS
coverage: 25.6% of statements
ok      github.com/mdbraber/lego/providers/dns/transip  0.013s
mdbraber-mbp:transip mdbraber$ git add test_private.key
mdbraber-mbp:transip mdbraber$ git add transip_test.go
mdbraber-mbp:transip mdbraber$ git commit -m "Update test"
[master c9eff5e] Update test
 2 files changed, 3 insertions(+), 4 deletions(-)
 create mode 100644 providers/dns/transip/test_private.key
ldez: Could you do a test with the following command and give me the output:

```bash
rm -rf ./lego
./lego -m your@email.com -a -x http-01 -x tls-alpn-01 --dns transip -s https://acme-staging-v02.api.letsencrypt.org/directory -d mydomain.com -d *.mydomain.com run
```



mdbraber: I've updated the domainName / subdomain logic, tests and cleaned up some other parts of the code. Please take a look at the domainName / subdomain logic - maybe there's a cleaner way, but this works too. Also updated the branch.

Here's the output of the command you requested:

```
mdbraber-mbp:lego mdbraber$ make build && TRANSIP_ACCOUNT_NAME="mdbraber" TRANSIP_PRIVATE_KEY_PATH="/Users/mdbraber/transip.key" ./lego -m m@mdbraber.com -a -x http-01 -x tls-alpn-01 --dns transip --dns-resolvers ns0.transip.nl -s https://acme-staging-v02.api.letsencrypt.org/directory -d mdbraber.net -d *.mdbraber.net run
rm -rf dist/ builds/ cover.out
go build
2018/11/06 09:12:03 [INFO] [mdbraber.net, *.mdbraber.net] acme: Obtaining bundled SAN certificate
2018/11/06 09:12:04 [INFO] [*.mdbraber.net] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz/6cJliCi531DXAnZtsGO3eGPpXZQVDqLlfunbsG_ZWJ8
2018/11/06 09:12:04 [INFO] [mdbraber.net] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz/BmgGpyhKMLhvUxIu__SXmkFgqsAFQmctYaZgXmXirTI
2018/11/06 09:12:04 [INFO] [mdbraber.net] acme: Authorization already valid; skipping challenge
2018/11/06 09:12:04 [INFO] [mdbraber.net] acme: Preparing to solve DNS-01
2018/11/06 09:12:05 [INFO] [mdbraber.net] acme: Trying to solve DNS-01
2018/11/06 09:12:05 [INFO] [mdbraber.net] Checking DNS record propagation using [ns0.transip.nl:53]
2018/11/06 09:12:05 [INFO] Wait [timeout: 10m0s, interval: 10s]
2018/11/06 09:12:20 [INFO] [mdbraber.net] The server validated our request
2018/11/06 09:12:22 [INFO] [mdbraber.net, *.mdbraber.net] acme: Validations succeeded; requesting certificates
2018/11/06 09:12:23 [INFO] [mdbraber.net] Server responded with a certificate.
```
ldez: ```
2018/11/06 09:12:04 [INFO] [mdbraber.net] acme: Authorization already valid; skipping challenge
```

could you retry?

```bash
# required before running the command (remove cache)
rm -rf ./lego

./lego -m your@email.com -a -x http-01 -x tls-alpn-01 --dns transip -s https://acme-staging-v02.api.letsencrypt.org/directory -d mydomain.com -d *.mydomain.com run
```


mdbraber: @ldez sorry, I still need to get my coffee ;-) Here's the new output:

```
mdbraber-mbp:lego mdbraber$ make build && TRANSIP_ACCOUNT_NAME="mdbraber" TRANSIP_PRIVATE_KEY_PATH="/Users/mdbraber/transip.key" ./lego -m m@mdbraber.com -a -x http-01 -x tls-alpn-01 --dns transip --dns-resolvers ns0.transip.nl -s https://acme-staging-v02.api.letsencrypt.org/directory -d mdbraber.net -d *.mdbraber.net run
rm -rf dist/ builds/ cover.out
go build
2018/11/06 09:57:34 [INFO] [mdbraber.net, *.mdbraber.net] acme: Obtaining bundled SAN certificate
2018/11/06 09:57:35 [INFO] [*.mdbraber.net] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz/hgORnAw2B-LC3e83vxLdQHz20IzV4JgOGr1R-Klqa8g
2018/11/06 09:57:35 [INFO] [mdbraber.net] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz/PFU_hbvXsMqs-PykFST2AwdocOVAJzMr_hdG92u3YUs
2018/11/06 09:57:35 [INFO] [mdbraber.net] acme: Preparing to solve DNS-01
2018/11/06 09:57:38 [INFO] [mdbraber.net] acme: Preparing to solve DNS-01
2018/11/06 09:57:39 [INFO] [mdbraber.net] acme: Trying to solve DNS-01
2018/11/06 09:57:39 [INFO] [mdbraber.net] Checking DNS record propagation using [ns0.transip.nl:53]
2018/11/06 09:57:39 [INFO] Wait [timeout: 10m0s, interval: 10s]
2018/11/06 10:00:15 [INFO] [mdbraber.net] The server validated our request
2018/11/06 10:00:15 [INFO] [mdbraber.net] acme: Trying to solve DNS-01
2018/11/06 10:00:15 [INFO] [mdbraber.net] Checking DNS record propagation using [ns0.transip.nl:53]
2018/11/06 10:00:15 [INFO] Wait [timeout: 10m0s, interval: 10s]
2018/11/06 10:00:20 [INFO] [mdbraber.net] The server validated our request
2018/11/06 10:00:22 [INFO] [mdbraber.net, *.mdbraber.net] acme: Validations succeeded; requesting certificates
2018/11/06 10:00:23 [INFO] [mdbraber.net] Server responded with a certificate.
```
ldez: @mdbraber I do not think a deprecated constructor needs to be introduced.
Use the `NewDNSProviderConfig` instead.
mdbraber: @ldez sorry, didn't know it was deprecated - removed now (I'm seeing most of the Caddy plugins are using the old constructor, but I'll update to the new one)
mdbraber: @ldez I'm not sure what to do with your latest commit - should I change anything?
mdbraber: Great - thanks for your quick review @ldez. Looking forward to the merge so this can be used in Caddy as well (if I didn't need local redirection I'd be happy to use Traefik ;-)

Patch:
diff --git a/Gopkg.lock b/Gopkg.lock
index 22b19271..ad376567 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -446,6 +446,18 @@
   pruneopts = "NUT"
   revision = "37e84520dcf74488f67654f9c775b9752c232dc1"
 
+[[projects]]
+  branch = "master"
+  digest = "1:3b236e8930d31aeb375fe405c15c2afc581e04bd6cb68da4723e1aa8d2e2da37"
+  name = "github.com/transip/gotransip"
+  packages = [
+    ".",
+    "domain",
+    "util",
+  ]
+  pruneopts = "NUT"
+  revision = "1dc93a7db3567a5ccf865106afac88278ba940cf"
+
 [[projects]]
   digest = "1:5dba68a1600a235630e208cb7196b24e58fcbb77bb7a6bec08fcd23f081b0a58"
   name = "github.com/urfave/cli"
@@ -659,6 +671,8 @@
     "github.com/stretchr/testify/suite",
     "github.com/timewasted/linode",
     "github.com/timewasted/linode/dns",
+    "github.com/transip/gotransip",
+    "github.com/transip/gotransip/domain",
     "github.com/urfave/cli",
     "golang.org/x/crypto/ocsp",
     "golang.org/x/net/context",
diff --git a/Gopkg.toml b/Gopkg.toml
index af2670a8..9c365a4c 100644
--- a/Gopkg.toml
+++ b/Gopkg.toml
@@ -85,6 +85,10 @@
   branch = "master"
   name = "github.com/sacloud/libsacloud"
 
+[[constraint]]
+  branch = "master"
+  name = "github.com/transip/gotransip"
+
 [[constraint]]
   version = "0.11.1"
   name = "github.com/exoscale/egoscale"
diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go
index 1d7672a4..4a50a8a3 100644
--- a/providers/dns/dns_providers.go
+++ b/providers/dns/dns_providers.go
@@ -48,6 +48,7 @@ import (
 	"github.com/xenolf/lego/providers/dns/sakuracloud"
 	"github.com/xenolf/lego/providers/dns/selectel"
 	"github.com/xenolf/lego/providers/dns/stackpath"
+	"github.com/xenolf/lego/providers/dns/transip"
 	"github.com/xenolf/lego/providers/dns/vegadns"
 	"github.com/xenolf/lego/providers/dns/vultr"
 )
@@ -145,6 +146,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
 		return stackpath.NewDNSProvider()
 	case "selectel":
 		return selectel.NewDNSProvider()
+	case "transip":
+		return transip.NewDNSProvider()
 	case "vegadns":
 		return vegadns.NewDNSProvider()
 	case "vultr":
diff --git a/providers/dns/transip/fixtures/private.key b/providers/dns/transip/fixtures/private.key
new file mode 100644
index 00000000..e69de29b
diff --git a/providers/dns/transip/transip.go b/providers/dns/transip/transip.go
new file mode 100644
index 00000000..aeb155f8
--- /dev/null
+++ b/providers/dns/transip/transip.go
@@ -0,0 +1,150 @@
+// Package transip implements a DNS provider for solving the DNS-01 challenge using TransIP.
+package transip
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/transip/gotransip"
+	transipdomain "github.com/transip/gotransip/domain"
+	"github.com/xenolf/lego/acme"
+	"github.com/xenolf/lego/platform/config/env"
+)
+
+// Config is used to configure the creation of the DNSProvider
+type Config struct {
+	AccountName        string
+	PrivateKeyPath     string
+	PropagationTimeout time.Duration
+	PollingInterval    time.Duration
+	TTL                int64
+}
+
+// NewDefaultConfig returns a default configuration for the DNSProvider
+func NewDefaultConfig() *Config {
+	return &Config{
+		TTL:                int64(env.GetOrDefaultInt("TRANSIP_TTL", 10)),
+		PropagationTimeout: env.GetOrDefaultSecond("TRANSIP_PROPAGATION_TIMEOUT", 10*time.Minute),
+		PollingInterval:    env.GetOrDefaultSecond("TRANSIP_POLLING_INTERVAL", 10*time.Second),
+	}
+}
+
+// DNSProvider describes a provider for TransIP
+type DNSProvider struct {
+	config *Config
+	client gotransip.SOAPClient
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for TransIP.
+// Credentials must be passed in the environment variables:
+// TRANSIP_ACCOUNTNAME, TRANSIP_PRIVATEKEYPATH.
+func NewDNSProvider() (*DNSProvider, error) {
+	values, err := env.Get("TRANSIP_ACCOUNT_NAME", "TRANSIP_PRIVATE_KEY_PATH")
+	if err != nil {
+		return nil, fmt.Errorf("transip: %v", err)
+	}
+
+	config := NewDefaultConfig()
+	config.AccountName = values["TRANSIP_ACCOUNT_NAME"]
+	config.PrivateKeyPath = values["TRANSIP_PRIVATE_KEY_PATH"]
+
+	return NewDNSProviderConfig(config)
+}
+
+// NewDNSProviderConfig return a DNSProvider instance configured for TransIP.
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+	if config == nil {
+		return nil, errors.New("transip: the configuration of the DNS provider is nil")
+	}
+
+	client, err := gotransip.NewSOAPClient(gotransip.ClientConfig{
+		AccountName:    config.AccountName,
+		PrivateKeyPath: config.PrivateKeyPath,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("transip: %v", err)
+	}
+
+	return &DNSProvider{client: client, config: config}, nil
+}
+
+// Timeout returns the timeout and interval to use when checking for DNS propagation.
+// Adjusting here to cope with spikes in propagation times.
+func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
+	return d.config.PropagationTimeout, d.config.PollingInterval
+}
+
+// Present creates a TXT record to fulfill the dns-01 challenge
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+	fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
+
+	authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+	if err != nil {
+		return err
+	}
+
+	domainName := acme.UnFqdn(authZone)
+
+	// get the subDomain
+	subDomain := strings.TrimSuffix(acme.UnFqdn(fqdn), "."+domainName)
+
+	// get all DNS entries
+	info, err := transipdomain.GetInfo(d.client, domainName)
+	if err != nil {
+		return fmt.Errorf("transip: error for %s in Present: %v", domain, err)
+	}
+
+	// include the new DNS entry
+	dnsEntries := append(info.DNSEntries, transipdomain.DNSEntry{
+		Name:    subDomain,
+		TTL:     d.config.TTL,
+		Type:    transipdomain.DNSEntryTypeTXT,
+		Content: value,
+	})
+
+	// set the updated DNS entries
+	err = transipdomain.SetDNSEntries(d.client, domainName, dnsEntries)
+	if err != nil {
+		return fmt.Errorf("transip: %v", err)
+	}
+	return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+	fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
+
+	authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+	if err != nil {
+		return err
+	}
+
+	domainName := acme.UnFqdn(authZone)
+
+	// get the subDomain
+	subDomain := strings.TrimSuffix(acme.UnFqdn(fqdn), "."+domainName)
+
+	// get all DNS entries
+	info, err := transipdomain.GetInfo(d.client, domainName)
+	if err != nil {
+		return fmt.Errorf("transip: error for %s in CleanUp: %v", fqdn, err)
+	}
+
+	// loop through the existing entries and remove the specific record
+	updatedEntries := info.DNSEntries[:0]
+	for _, e := range info.DNSEntries {
+		if e.Name != subDomain {
+			updatedEntries = append(updatedEntries, e)
+		}
+	}
+
+	// set the updated DNS entries
+	err = transipdomain.SetDNSEntries(d.client, domainName, updatedEntries)
+	if err != nil {
+		return fmt.Errorf("transip: couldn't get Record ID in CleanUp: %sv", err)
+	}
+
+	return nil
+}
diff --git a/vendor/github.com/transip/gotransip/LICENSE b/vendor/github.com/transip/gotransip/LICENSE
new file mode 100644
index 00000000..352d193e
--- /dev/null
+++ b/vendor/github.com/transip/gotransip/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 TransIP B.V.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/transip/gotransip/api.go b/vendor/github.com/transip/gotransip/api.go
new file mode 100644
index 00000000..46c68448
--- /dev/null
+++ b/vendor/github.com/transip/gotransip/api.go
@@ -0,0 +1,12 @@
+package gotransip
+
+// CancellationTime represents the possible ways of canceling a contract
+type CancellationTime string
+
+var (
+	// CancellationTimeEnd specifies to cancel the contract when the contract was
+	// due to end anyway
+	CancellationTimeEnd CancellationTime = "end"
+	// CancellationTimeImmediately specifies to cancel the contract immediately
+	CancellationTimeImmediately CancellationTime = "immediately"
+)
diff --git a/vendor/github.com/transip/gotransip/client.go b/vendor/github.com/transip/gotransip/client.go
new file mode 100644
index 00000000..0be9c400
--- /dev/null
+++ b/vendor/github.com/transip/gotransip/client.go
@@ -0,0 +1,119 @@
+package gotransip
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+)
+
+const (
+	transipAPIHost      = "api.transip.nl"
+	transipAPINamespace = "http://www.transip.nl/soap"
+)
+
+// APIMode specifies in which mode the API is used. Currently this is only
+// supports either readonly or readwrite
+type APIMode string
+
+var (
+	// APIModeReadOnly specifies that no changes can be made from API calls
+	APIModeReadOnly APIMode = "readonly"
+	// APIModeReadWrite specifies that changes can be made from API calls
+	APIModeReadWrite APIMode = "readwrite"
+)
+
+// ClientConfig is a tool to easily create a new Client object
+type ClientConfig struct {
+	AccountName    string
+	PrivateKeyPath string
+	PrivateKeyBody []byte
+	Mode           APIMode
+}
+
+// Client is the interface which all clients should implement
+type Client interface {
+	Call(SoapRequest, interface{}) error // execute request on client
+}
+
+// SOAPClient represents a TransIP API SOAP client, implementing the Client
+// interface
+type SOAPClient struct {
+	soapClient soapClient
+}
+
+// Call performs given SOAP request and fills the response into result
+func (c SOAPClient) Call(req SoapRequest, result interface{}) error {
+	return c.soapClient.call(req, result)
+}
+
+// NewSOAPClient returns a new SOAPClient object for given config
+// ClientConfig's PrivateKeyPath will override potentially given PrivateKeyBody
+func NewSOAPClient(c ClientConfig) (SOAPClient, error) {
+	// check account name
+	if len(c.AccountName) == 0 {
+		return SOAPClient{}, errors.New("AccountName is required")
+	}
+
+	// check if private key was given in any form
+	if len(c.PrivateKeyPath) == 0 && len(c.PrivateKeyBody) == 0 {
+		return SOAPClient{}, errors.New("PrivateKeyPath or PrivateKeyBody is required")
+	}
+
+	// if PrivateKeyPath was set, this will override any given PrivateKeyBody
+	if len(c.PrivateKeyPath) > 0 {
+		// try to open private key and read contents
+		if _, err := os.Stat(c.PrivateKeyPath); err != nil {
+			return SOAPClient{}, fmt.Errorf("could not open private key: %s", err.Error())
+		}
+
+		// read private key so we can pass the body to the soapClient
+		var err error
+		c.PrivateKeyBody, err = ioutil.ReadFile(c.PrivateKeyPath)
+		if err != nil {
+			return SOAPClient{}, err
+		}
+	}
+
+	// default to APIMode read/write
+	if len(c.Mode) == 0 {
+		c.Mode = APIModeReadWrite
+	}
+
+	// create soapClient and pass it to a new Client pointer
+	sc := soapClient{
+		Login:      c.AccountName,
+		Mode:       c.Mode,
+		PrivateKey: c.PrivateKeyBody,
+	}
+
+	return SOAPClient{
+		soapClient: sc,
+	}, nil
+}
+
+// FakeSOAPClient is a client doing nothing except implementing the gotransip.Client
+// interface
+// you can however set a fixture XML body which Call will try to Unmarshal into
+// result
+// useful for testing
+type FakeSOAPClient struct {
+	fixture []byte // preset this fixture so Call can use it to Unmarshal
+}
+
+// FixtureFromFile reads file and sets content as FakeSOAPClient's fixture
+func (f *FakeSOAPClient) FixtureFromFile(file string) (err error) {
+	// read fixture file
+	f.fixture, err = ioutil.ReadFile(file)
+	if err != nil {
+		err = fmt.Errorf("could not read fixture from file %s: %s", file, err.Error())
+	}
+
+	return
+}
+
+// Call doesn't do anything except fill the XML unmarshalled result
+func (f FakeSOAPClient) Call(req SoapRequest, result interface{}) error {
+	// this fake client just parses given fixture as if it was a SOAP response
+	return parseSoapResponse(f.fixture, req.Padding, 200, result)
+}
diff --git a/vendor/github.com/transip/gotransip/domain/api.go b/vendor/github.com/transip/gotransip/domain/api.go
new file mode 100644
index 00000000..f5f90f8e
--- /dev/null
+++ b/vendor/github.com/transip/gotransip/domain/api.go
@@ -0,0 +1,314 @@
+package domain
+
+import (
+	"github.com/transip/gotransip"
+)
+
+// This file holds all DomainService methods directly ported from TransIP API
+
+// BatchCheckAvailability checks the availability of multiple domains
+func BatchCheckAvailability(c gotransip.Client, domainNames []string) ([]CheckResult, error) {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "batchCheckAvailability",
+	}
+	sr.AddArgument("domainNames", domainNames)
+
+	var v struct {
+		V []CheckResult `xml:"item"`
+	}
+
+	err := c.Call(sr, &v)
+	return v.V, err
+}
+
+// CheckAvailability returns the availability status of a domain.
+func CheckAvailability(c gotransip.Client, domainName string) (Status, error) {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "checkAvailability",
+	}
+	sr.AddArgument("domainName", domainName)
+
+	var v Status
+	err := c.Call(sr, &v)
+	return v, err
+}
+
+// GetWhois returns the whois of a domain name
+func GetWhois(c gotransip.Client, domainName string) (string, error) {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "getWhois",
+	}
+	sr.AddArgument("domainName", domainName)
+
+	var v string
+	err := c.Call(sr, &v)
+	return v, err
+}
+
+// GetDomainNames returns list with domain names or error when this failed
+func GetDomainNames(c gotransip.Client) ([]string, error) {
+	var d = struct {
+		D []string `xml:"item"`
+	}{}
+	err := c.Call(gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "getDomainNames",
+	}, &d)
+
+	return d.D, err
+}
+
+// GetInfo returns Domain for given name or error when this failed
+func GetInfo(c gotransip.Client, domainName string) (Domain, error) {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "getInfo",
+	}
+	sr.AddArgument("domainName", domainName)
+
+	var d Domain
+	err := c.Call(sr, &d)
+
+	return d, err
+}
+
+// BatchGetInfo returns array of Domain for given name or error when this failed
+func BatchGetInfo(c gotransip.Client, domainNames []string) ([]Domain, error) {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "batchGetInfo",
+	}
+	sr.AddArgument("domainNames", domainNames)
+
+	var d = struct {
+		D []Domain `xml:"item"`
+	}{}
+	err := c.Call(sr, &d)
+
+	return d.D, err
+}
+
+// GetAuthCode returns the Auth code for a domainName
+func GetAuthCode(c gotransip.Client, domainName string) (string, error) {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "getAuthCode",
+	}
+	sr.AddArgument("domainName", domainName)
+
+	var v string
+	err := c.Call(sr, &v)
+	return v, err
+}
+
+// GetIsLocked returns the lock status for a domainName
+func GetIsLocked(c gotransip.Client, domainName string) (bool, error) {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "getIsLocked",
+	}
+	sr.AddArgument("domainName", domainName)
+
+	var v bool
+	err := c.Call(sr, &v)
+	return v, err
+}
+
+// Register registers a domain name and will automatically create and sign a proposition for it
+func Register(c gotransip.Client, domain string) error {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "register",
+	}
+	sr.AddArgument("domain", domain)
+
+	return c.Call(sr, nil)
+}
+
+// Cancel cancels a domain name, will automatically create and sign a cancellation document
+func Cancel(c gotransip.Client, domainName string, endTime gotransip.CancellationTime) error {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "cancel",
+	}
+	sr.AddArgument("domainName", domainName)
+	sr.AddArgument("endTime", string(endTime))
+
+	return c.Call(sr, nil)
+}
+
+// TransferWithOwnerChange transfers a domain with changing the owner
+func TransferWithOwnerChange(c gotransip.Client, domain, authCode string) error {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "transferWithOwnerChange",
+	}
+	sr.AddArgument("domain", domain)
+	sr.AddArgument("authCode", authCode)
+
+	return c.Call(sr, nil)
+}
+
+// TransferWithoutOwnerChange transfers a domain without changing the owner
+func TransferWithoutOwnerChange(c gotransip.Client, domain, authCode string) error {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "transferWithoutOwnerChange",
+	}
+	sr.AddArgument("domain", domain)
+	sr.AddArgument("authCode", authCode)
+
+	return c.Call(sr, nil)
+}
+
+// SetNameservers starts a nameserver change for this domain, will replace all
+// existing nameservers with the new nameservers
+func SetNameservers(c gotransip.Client, domainName string, nameservers Nameservers) error {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "setNameservers",
+	}
+	sr.AddArgument("domainName", domainName)
+	sr.AddArgument("nameservers", nameservers)
+
+	return c.Call(sr, nil)
+}
+
+// SetLock locks this domain
+func SetLock(c gotransip.Client, domainName string) error {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "setLock",
+	}
+	sr.AddArgument("domainName", domainName)
+
+	return c.Call(sr, nil)
+}
+
+// UnsetLock unlocks this domain
+func UnsetLock(c gotransip.Client, domainName string) error {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "unsetLock",
+	}
+	sr.AddArgument("domainName", domainName)
+
+	return c.Call(sr, nil)
+}
+
+// SetDNSEntries sets the DnsEntries for this Domain, will replace all existing
+// dns entries with the new entries
+func SetDNSEntries(c gotransip.Client, domainName string, dnsEntries DNSEntries) error {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "setDnsEntries",
+	}
+	sr.AddArgument("domainName", domainName)
+	sr.AddArgument("dnsEntries", dnsEntries)
+
+	return c.Call(sr, nil)
+}
+
+// SetOwner starts an owner change of a domain
+func SetOwner(c gotransip.Client, domainName, registrantWhoisContact WhoisContact) error {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "setOwner",
+	}
+	sr.AddArgument("domainName", domainName)
+	// make sure contact is of type registrant
+	registrantWhoisContact.Type = "registrant"
+	sr.AddArgument("registrantWhoisContact", registrantWhoisContact)
+
+	return c.Call(sr, nil)
+}
+
+// SetContacts starts a contact change of a domain, this will replace all existing contacts
+func SetContacts(c gotransip.Client, domainName, contacts WhoisContacts) error {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "setContacts",
+	}
+	sr.AddArgument("domainName", domainName)
+	sr.AddArgument("contacts", contacts)
+
+	return c.Call(sr, nil)
+}
+
+// GetAllTLDInfos returns slice with TLD objects or error when this failed
+func GetAllTLDInfos(c gotransip.Client) ([]TLD, error) {
+	var d = struct {
+		TLD []TLD `xml:"item"`
+	}{}
+	err := c.Call(gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "getAllTldInfos",
+	}, &d)
+
+	return d.TLD, err
+}
+
+// GetTldInfo returns info about a specific TLD
+func GetTldInfo(c gotransip.Client, tldName string) (TLD, error) {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "getTldInfo",
+	}
+	sr.AddArgument("tldName", tldName)
+
+	var v TLD
+	err := c.Call(sr, &v)
+	return v, err
+}
+
+// GetCurrentDomainAction returns info about the action this domain is currently running
+func GetCurrentDomainAction(c gotransip.Client, domainName string) (ActionResult, error) {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "getCurrentDomainAction",
+	}
+	sr.AddArgument("domainName", domainName)
+
+	var v ActionResult
+	err := c.Call(sr, &v)
+	return v, err
+}
+
+// RetryCurrentDomainActionWithNewData retries a failed domain action with new
+// domain data. The Domain.Name field must contain the name of the Domain. The
+// Nameservers, Contacts, DNSEntries fields contain the new data for this domain.
+func RetryCurrentDomainActionWithNewData(c gotransip.Client, domain Domain) error {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "retryCurrentDomainActionWithNewData",
+	}
+	sr.AddArgument("domain", domain)
+
+	return c.Call(sr, nil)
+}
+
+// RetryTransferWithDifferentAuthCode retries a transfer action with a new authcode
+func RetryTransferWithDifferentAuthCode(c gotransip.Client, domain Domain, newAuthCode string) error {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "retryTransferWithDifferentAuthCode",
+	}
+	sr.AddArgument("domain", domain)
+	sr.AddArgument("newAuthCode", newAuthCode)
+
+	return c.Call(sr, nil)
+}
+
+// CancelDomainAction cancels a failed domain action
+func CancelDomainAction(c gotransip.Client, domain string) error {
+	sr := gotransip.SoapRequest{
+		Service: serviceName,
+		Method:  "cancelDomainAction",
+	}
+	sr.AddArgument("domain", domain)
+
+	return c.Call(sr, nil)
+}
diff --git a/vendor/github.com/transip/gotransip/domain/domain.go b/vendor/github.com/transip/gotransip/domain/domain.go
new file mode 100644
index 00000000..56d7b719
--- /dev/null
+++ b/vendor/github.com/transip/gotransip/domain/domain.go
@@ -0,0 +1,405 @@
+package domain
+
+import (
+	"fmt"
+	"net"
+
+	"github.com/transip/gotransip"
+	"github.com/transip/gotransip/util"
+)
+
+const (
+	serviceName string = "DomainService"
+)
+
+// Domain represents a Transip_Domain object
+// as described at https://api.transip.nl/docs/transip.nl/class-Transip_Domain.html
+type Domain struct {
+	Name              string         `xml:"name"`
+	Nameservers       []Nameserver   `xml:"nameservers>item"`
+	Contacts          []WhoisContact `xml:"contacts>item"`
+	DNSEntries        []DNSEntry     `xml:"dnsEntries>item"`
+	Branding          Branding       `xml:"branding"`
+	AuthorizationCode string         `xml:"authCode"`
+	IsLocked          bool           `xml:"isLocked"`
+	RegistrationDate  util.XMLTime   `xml:"registrationDate"`
+	RenewalDate       util.XMLTime   `xml:"renewalDate"`
+}
+
+// EncodeParams returns Domain parameters ready to be used for constructing a signature
+func (d Domain) EncodeParams(prm gotransip.ParamsContainer) {
+	idx := prm.Len()
+	prm.Add(fmt.Sprintf("%d[name]", idx), d.Name)
+	prm.Add(fmt.Sprintf("%d[authCode]", idx), d.AuthorizationCode)
+	prm.Add(fmt.Sprintf("%d[isLocked]", idx), fmt.Sprintf("%t", d.IsLocked))
+	prm.Add(fmt.Sprintf("%d[registrationDate]", idx), d.RegistrationDate.Format("2006-01-02"))
+	prm.Add(fmt.Sprintf("%d[renewalDate]", idx), d.RenewalDate.Format("2006-01-02"))
+	// nameservers
+	for i, e := range d.Nameservers {
+		var ipv4, ipv6 string
+		if e.IPv4Address != nil {
+			ipv4 = e.IPv4Address.String()
+		}
+		if e.IPv6Address != nil {
+			ipv6 = e.IPv6Address.String()
+		}
+		prm.Add(fmt.Sprintf("%d[nameservers][%d][hostname]", idx, i), e.Hostname)
+		prm.Add(fmt.Sprintf("%d[nameservers][%d][ipv4]", idx, i), ipv4)
+		prm.Add(fmt.Sprintf("%d[nameservers][%d][ipv6]", idx, i), ipv6)
+	}
+	// contacts
+	for i, e := range d.Contacts {
+		prm.Add(fmt.Sprintf("%d[contacts][%d][type]", idx, i), e.Type)
+		prm.Add(fmt.Sprintf("%d[contacts][%d][firstName]", idx, i), e.FirstName)
+		prm.Add(fmt.Sprintf("%d[contacts][%d][middleName]", idx, i), e.MiddleName)
+		prm.Add(fmt.Sprintf("%d[contacts][%d][lastName]", idx, i), e.LastName)
+		prm.Add(fmt.Sprintf("%d[contacts][%d][companyName]", idx, i), e.CompanyName)
+		prm.Add(fmt.Sprintf("%d[contacts][%d][companyKvk]", idx, i), e.CompanyKvk)
+		prm.Add(fmt.Sprintf("%d[contacts][%d][companyType]", idx, i), e.CompanyType)
+		prm.Add(fmt.Sprintf("%d[contacts][%d][street]", idx, i), e.Street)
+		prm.Add(fmt.Sprintf("%d[contacts][%d][number]", idx, i), e.Number)
+		prm.Add(fmt.Sprintf("%d[contacts][%d][postalCode]", idx, i), e.PostalCode)
+		prm.Add(fmt.Sprintf("%d[contacts][%d][city]", idx, i), e.City)
+		prm.Add(fmt.Sprintf("%d[contacts][%d][phoneNumber]", idx, i), e.PhoneNumber)
+		prm.Add(fmt.Sprintf("%d[contacts][%d][faxNumber]", idx, i), e.FaxNumber)
+		prm.Add(fmt.Sprintf("%d[contacts][%d][email]", idx, i), e.Email)
+		prm.Add(fmt.Sprintf("%d[contacts][%d][country]", idx, i), e.Country)
+	}
+	// dnsEntries
+	for i, e := range d.DNSEntries {
+		prm.Add(fmt.Sprintf("%d[dnsEntries][%d][name]", idx, i), e.Name)
+		prm.Add(fmt.Sprintf("%d[dnsEntries][%d][expire]", idx, i), fmt.Sprintf("%d", e.TTL))
+		prm.Add(fmt.Sprintf("%d[dnsEntries][%d][type]", idx, i), string(e.Type))
+		prm.Add(fmt.Sprintf("%d[dnsEntries][%d][content]", idx, i), e.Content)
+	}
+	// branding
+	prm.Add(fmt.Sprintf("%d[branding][companyName]", idx), d.Branding.CompanyName)
+	prm.Add(fmt.Sprintf("%d[branding][supportEmail]", idx), d.Branding.SupportEmail)
+	prm.Add(fmt.Sprintf("%d[branding][companyUrl]", idx), d.Branding.CompanyURL)
+	prm.Add(fmt.Sprintf("%d[branding][termsOfUsageUrl]", idx), d.Branding.TermsOfUsageURL)
+	prm.Add(fmt.Sprintf("%d[branding][bannerLine1]", idx), d.Branding.BannerLine1)
+	prm.Add(fmt.Sprintf("%d[branding][bannerLine2]", idx), d.Branding.BannerLine2)
+	prm.Add(fmt.Sprintf("%d[branding][bannerLine3]", idx), d.Branding.BannerLine3)
+}
+
+// EncodeArgs returns Domain XML body ready to be passed in the SOAP call
+func (d Domain) EncodeArgs(key string) string {
+	output := fmt.Sprintf(`<%s xsi:type="ns1:Domain">
+	<name xsi:type="xsd:string">%s</name>
+	<authCode xsi:type="xsd:string">%s</authCode>
+	<isLocked xsi:type="xsd:boolean">%t</isLocked>
+	<registrationDate xsi:type="xsd:string">%s</registrationDate>
+	<renewalDate xsi:type="xsd:string">%s</renewalDate>`,
+		key, d.Name, d.AuthorizationCode, d.IsLocked,
+		d.RegistrationDate.Format("2006-01-02"), d.RenewalDate.Format("2006-01-02"),
+	) + "\n"
+
+	output += Nameservers(d.Nameservers).EncodeArgs("nameservers") + "\n"
+	output += WhoisContacts(d.Contacts).EncodeArgs("contacts") + "\n"
+	output += DNSEntries(d.DNSEntries).EncodeArgs("dnsEntries") + "\n"
+	output += d.Branding.EncodeArgs("branding") + "\n"
+
+	return fmt.Sprintf("%s</%s>", output, key)
+}
+
+// Capability represents the possible capabilities a TLD can have
+type Capability string
+
+var (
+	// CapabilityRequiresAuthCode defines this TLD requires an auth code
+	// to be transferred
+	CapabilityRequiresAuthCode Capability = "requiresAuthCode"
+	// CapabilityCanRegister defines this TLD can be registered
+	CapabilityCanRegister Capability = "canRegister"
+	// CapabilityCanTransferWithOwnerChange defines this TLD can be transferred
+	// with change of ownership
+	CapabilityCanTransferWithOwnerChange Capability = "canTransferWithOwnerChange"
+	// CapabilityCanTransferWithoutOwnerChange defines this TLD can be
+	// transferred without change of ownership
+	CapabilityCanTransferWithoutOwnerChange Capability = "canTransferWithoutOwnerChange"
+	// CapabilityCanSetLock defines this TLD allows to be locked
+	CapabilityCanSetLock Capability = "canSetLock"
+	// CapabilityCanSetOwner defines this TLD supports setting an owner
+	CapabilityCanSetOwner Capability = "canSetOwner"
+	// CapabilityCanSetContacts defines this TLD supports setting contacts
+	CapabilityCanSetContacts Capability = "canSetContacts"
+	// CapabilityCanSetNameservers defines this TLD supports setting nameservers
+	CapabilityCanSetNameservers Capability = "canSetNameservers"
+)
+
+// TLD represents a Transip_Tld object as described at
+// https://api.transip.nl/docs/transip.nl/class-Transip_Tld.html
+type TLD struct {
+	Name                     string       `xml:"name"`
+	Price                    float64      `xml:"price"`
+	RenewalPrice             float64      `xml:"renewalPrice"`
+	Capabilities             []Capability `xml:"capabilities>item"`
+	RegistrationPeriodLength int64        `xml:"registrationPeriodLength"`
+	CancelTimeFrame          int64        `xml:"cancelTimeFrame"`
+}
+
+// Nameserver represents a Transip_Nameserver object as described at
+// https://api.transip.nl/docs/transip.nl/class-Transip_Nameserver.html
+type Nameserver struct {
+	Hostname    string `xml:"hostname"`
+	IPv4Address net.IP `xml:"ipv4"`
+	IPv6Address net.IP `xml:"ipv6"`
+}
+
+// Nameservers is just an array of Nameserver
+// basically only here so it can implement paramsEncoder
+type Nameservers []Nameserver
+
+// EncodeParams returns Nameservers parameters ready to be used for constructing a signature
+func (n Nameservers) EncodeParams(prm gotransip.ParamsContainer) {
+	idx := prm.Len()
+	for i, e := range n {
+		var ipv4, ipv6 string
+		if e.IPv4Address != nil {
+			ipv4 = e.IPv4Address.String()
+		}
+		if e.IPv6Address != nil {
+			ipv6 = e.IPv6Address.String()
+		}
+		prm.Add(fmt.Sprintf("%d[%d][hostname]", idx, i), e.Hostname)
+		prm.Add(fmt.Sprintf("%d[%d][ipv4]", idx, i), ipv4)
+		prm.Add(fmt.Sprintf("%d[%d][ipv6]", idx, i), ipv6)
+	}
+}
+
+// EncodeArgs returns Nameservers XML body ready to be passed in the SOAP call
+func (n Nameservers) EncodeArgs(key string) string {
+	output := fmt.Sprintf(`<%s SOAP-ENC:arrayType="ns1:Nameserver[%d]" xsi:type="ns1:ArrayOfNameserver">`, key, len(n)) + "\n"
+	for _, e := range n {
+		var ipv4, ipv6 string
+		if e.IPv4Address != nil {
+			ipv4 = e.IPv4Address.String()
+		}
+		if e.IPv6Address != nil {
+			ipv6 = e.IPv6Address.String()
+		}
+		output += fmt.Sprintf(`	<item xsi:type="ns1:Nameserver">
+		<hostname xsi:type="xsd:string">%s</hostname>
+		<ipv4 xsi:type="xsd:string">%s</ipv4>
+		<ipv6 xsi:type="xsd:string">%s</ipv6>
+	</item>`, e.Hostname, ipv4, ipv6) + "\n"
+	}
+
+	return fmt.Sprintf("%s</%s>", output, key)
+}
+
+// DNSEntryType represents the possible types of DNS entries
+type DNSEntryType string
+
+var (
+	// DNSEntryTypeA represents an A-record
+	DNSEntryTypeA DNSEntryType = "A"
+	// DNSEntryTypeAAAA represents an AAAA-record
+	DNSEntryTypeAAAA DNSEntryType = "AAAA"
+	// DNSEntryTypeCNAME represents a CNAME-record
+	DNSEntryTypeCNAME DNSEntryType = "CNAME"
+	// DNSEntryTypeMX represents an MX-record
+	DNSEntryTypeMX DNSEntryType = "MX"
+	// DNSEntryTypeNS represents an NS-record
+	DNSEntryTypeNS DNSEntryType = "NS"
+	// DNSEntryTypeTXT represents a TXT-record
+	DNSEntryTypeTXT DNSEntryType = "TXT"
+	// DNSEntryTypeSRV represents an SRV-record
+	DNSEntryTypeSRV DNSEntryType = "SRV"
+)
+
+// DNSEntry represents a Transip_DnsEntry object as described at
+// https://api.transip.nl/docs/transip.nl/class-Transip_DnsEntry.html
+type DNSEntry struct {
+	Name    string       `xml:"name"`
+	TTL     int64        `xml:"expire"`
+	Type    DNSEntryType `xml:"type"`
+	Content string       `xml:"content"`
+}
+
+// DNSEntries is just an array of DNSEntry
+// basically only here so it can implement paramsEncoder
+type DNSEntries []DNSEntry
+
+// EncodeParams returns DNSEntries parameters ready to be used for constructing a signature
+func (d DNSEntries) EncodeParams(prm gotransip.ParamsContainer) {
+	idx := prm.Len()
+	for i, e := range d {
+		prm.Add(fmt.Sprintf("%d[%d][name]", idx, i), e.Name)
+		prm.Add(fmt.Sprintf("%d[%d][expire]", idx, i), fmt.Sprintf("%d", e.TTL))
+		prm.Add(fmt.Sprintf("%d[%d][type]", idx, i), string(e.Type))
+		prm.Add(fmt.Sprintf("%d[%d][content]", idx, i), e.Content)
+	}
+}
+
+// EncodeArgs returns DNSEntries XML body ready to be passed in the SOAP call
+func (d DNSEntries) EncodeArgs(key string) string {
+	output := fmt.Sprintf(`<%s SOAP-ENC:arrayType="ns1:DnsEntry[%d]" xsi:type="ns1:ArrayOfDnsEntry">`, key, len(d)) + "\n"
+	for _, e := range d {
+		output += fmt.Sprintf(`	<item xsi:type="ns1:DnsEntry">
+		<name xsi:type="xsd:string">%s</name>
+		<expire xsi:type="xsd:int">%d</expire>
+		<type xsi:type="xsd:string">%s</type>
+		<content xsi:type="xsd:string">%s</content>
+	</item>`, e.Name, e.TTL, e.Type, e.Content) + "\n"
+	}
+
+	return fmt.Sprintf("%s</%s>", output, key)
+}
+
+// Status reflects the current status of a domain in a check result
+type Status string
+
+var (
+	// StatusInYourAccount means he domain name is already in your account
+	StatusInYourAccount Status = "inyouraccount"
+	// StatusUnavailable means the domain name is currently unavailable and can not be registered due to unknown reasons.
+	StatusUnavailable Status = "unavailable"
+	// StatusNotFree means the domain name has already been registered
+	StatusNotFree Status = "notfree"
+	// StatusFree means the domain name is currently free, is available and can be registered
+	StatusFree Status = "free"
+	// StatusInternalPull means the domain name is currently registered at TransIP and is available to be pulled from another account to yours.
+	StatusInternalPull Status = "internalpull"
+	// StatusInternalPush means the domain name is currently registered at TransIP in your accounta and is available to be pushed to another account.
+	StatusInternalPush Status = "internalpush"
+)
+
+// CheckResult represents a Transip_DomainCheckResult object as described at
+// https://api.transip.nl/docs/transip.nl/class-Transip_DomainCheckResult.html
+type CheckResult struct {
+	DomainName string   `xml:"domainName"`
+	Status     Status   `xml:"status"`
+	Actions    []Action `xml:"actions>item"`
+}
+
+// Branding represents a Transip_DomainBranding object as described at
+// https://api.transip.nl/docs/transip.nl/class-Transip_DomainBranding.html
+type Branding struct {
+	CompanyName     string `xml:"companyName"`
+	SupportEmail    string `xml:"supportEmail"`
+	CompanyURL      string `xml:"companyUrl"`
+	TermsOfUsageURL string `xml:"termsOfUsageUrl"`
+	BannerLine1     string `xml:"bannerLine1"`
+	BannerLine2     string `xml:"bannerLine2"`
+	BannerLine3     string `xml:"bannerLine3"`
+}
+
+// EncodeParams returns WhoisContacts parameters ready to be used for constructing a signature
+func (b Branding) EncodeParams(prm gotransip.ParamsContainer) {
+	idx := prm.Len()
+	prm.Add(fmt.Sprintf("%d[companyName]", idx), b.CompanyName)
+	prm.Add(fmt.Sprintf("%d[supportEmail]", idx), b.SupportEmail)
+	prm.Add(fmt.Sprintf("%d[companyUrl]", idx), b.CompanyURL)
+	prm.Add(fmt.Sprintf("%d[termsOfUsageUrl]", idx), b.TermsOfUsageURL)
+	prm.Add(fmt.Sprintf("%d[bannerLine1]", idx), b.BannerLine1)
+	prm.Add(fmt.Sprintf("%d[bannerLine2]", idx), b.BannerLine2)
+	prm.Add(fmt.Sprintf("%d[bannerLine3]", idx), b.BannerLine3)
+}
+
+// EncodeArgs returns Branding XML body ready to be passed in the SOAP call
+func (b Branding) EncodeArgs(key string) string {
+	return fmt.Sprintf(`<branding xsi:type="ns1:DomainBranding">
+		<companyName xsi:type="xsd:string">%s</companyName>
+		<supportEmail xsi:type="xsd:string">%s</supportEmail>
+		<companyUrl xsi:type="xsd:string">%s</companyUrl>
+		<termsOfUsageUrl xsi:type="xsd:string">%s</termsOfUsageUrl>
+		<bannerLine1 xsi:type="xsd:string">%s</bannerLine1>
+		<bannerLine2 xsi:type="xsd:string">%s</bannerLine2>
+		<bannerLine3 xsi:type="xsd:string">%s</bannerLine3>
+	</branding>`, b.CompanyName, b.SupportEmail, b.CompanyURL, b.TermsOfUsageURL, b.BannerLine1, b.BannerLine2, b.BannerLine3)
+}
+
+// Action reflects the available actions to perform on a domain
+type Action string
+
+var (
+	// ActionRegister registers a domain
+	ActionRegister Action = "register"
+	// ActionTransfer transfers a domain to another provider
+	ActionTransfer Action = "transfer"
+	// ActionInternalPull transfers a domain to another account at TransIP
+	ActionInternalPull Action = "internalpull"
+)
+
+// ActionResult represents a Transip_DomainAction object as described at
+// https://api.transip.nl/docs/transip.nl/class-Transip_DomainAction.html
+type ActionResult struct {
+	Name      string `xml:"name"`
+	HasFailed bool   `xml:"hasFailed"`
+	Message   string `xml:"message"`
+}
+
+// WhoisContact represents a TransIP_WhoisContact object
+// as described at https://api.transip.nl/docs/transip.nl/class-Transip_WhoisContact.html
+type WhoisContact struct {
+	Type        string `xml:"type"`
+	FirstName   string `xml:"firstName"`
+	MiddleName  string `xml:"middleName"`
+	LastName    string `xml:"lastName"`
+	CompanyName string `xml:"companyName"`
+	CompanyKvk  string `xml:"companyKvk"`
+	CompanyType string `xml:"companyType"`
+	Street      string `xml:"street"`
+	Number      string `xml:"number"`
+	PostalCode  string `xml:"postalCode"`
+	City        string `xml:"city"`
+	PhoneNumber string `xml:"phoneNumber"`
+	FaxNumber   string `xml:"faxNumber"`
+	Email       string `xml:"email"`
+	Country     string `xml:"country"`
+}
+
+// WhoisContacts is just an array of WhoisContact
+// basically only here so it can implement paramsEncoder
+type WhoisContacts []WhoisContact
+
+// EncodeParams returns WhoisContacts parameters ready to be used for constructing a signature
+func (w WhoisContacts) EncodeParams(prm gotransip.ParamsContainer) {
+	idx := prm.Len()
+	for i, e := range w {
+		prm.Add(fmt.Sprintf("%d[%d][type]", idx, i), e.Type)
+		prm.Add(fmt.Sprintf("%d[%d][firstName]", idx, i), e.FirstName)
+		prm.Add(fmt.Sprintf("%d[%d][middleName]", idx, i), e.MiddleName)
+		prm.Add(fmt.Sprintf("%d[%d][lastName]", idx, i), e.LastName)
+		prm.Add(fmt.Sprintf("%d[%d][companyName]", idx, i), e.CompanyName)
+		prm.Add(fmt.Sprintf("%d[%d][companyKvk]", idx, i), e.CompanyKvk)
+		prm.Add(fmt.Sprintf("%d[%d][companyType]", idx, i), e.CompanyType)
+		prm.Add(fmt.Sprintf("%d[%d][street]", idx, i), e.Street)
+		prm.Add(fmt.Sprintf("%d[%d][number]", idx, i), e.Number)
+		prm.Add(fmt.Sprintf("%d[%d][postalCode]", idx, i), e.PostalCode)
+		prm.Add(fmt.Sprintf("%d[%d][city]", idx, i), e.City)
+		prm.Add(fmt.Sprintf("%d[%d][phoneNumber]", idx, i), e.PhoneNumber)
+		prm.Add(fmt.Sprintf("%d[%d][faxNumber]", idx, i), e.FaxNumber)
+		prm.Add(fmt.Sprintf("%d[%d][email]", idx, i), e.Email)
+		prm.Add(fmt.Sprintf("%d[%d][country]", idx, i), e.Country)
+	}
+}
+
+// EncodeArgs returns WhoisContacts XML body ready to be passed in the SOAP call
+func (w WhoisContacts) EncodeArgs(key string) string {
+	output := fmt.Sprintf(`<%s SOAP-ENC:arrayType="ns1:WhoisContact[%d]" xsi:type="ns1:ArrayOfWhoisContact">`, key, len(w)) + "\n"
+	for _, e := range w {
+		output += fmt.Sprintf(`	<item xsi:type="ns1:WhoisContact">
+		<type xsi:type="xsd:string">%s</type>
+		<firstName xsi:type="xsd:string">%s</firstName>
+		<middleName xsi:type="xsd:string">%s</middleName>
+		<lastName xsi:type="xsd:string">%s</lastName>
+		<companyName xsi:type="xsd:string">%s</companyName>
+		<companyKvk xsi:type="xsd:string">%s</companyKvk>
+		<companyType xsi:type="xsd:string">%s</companyType>
+		<street xsi:type="xsd:string">%s</street>
+		<number xsi:type="xsd:string">%s</number>
+		<postalCode xsi:type="xsd:string">%s</postalCode>
+		<city xsi:type="xsd:string">%s</city>
+		<phoneNumber xsi:type="xsd:string">%s</phoneNumber>
+		<faxNumber xsi:type="xsd:string">%s</faxNumber>
+		<email xsi:type="xsd:string">%s</email>
+		<country xsi:type="xsd:string">%s</country>
+	</item>`, e.Type, e.FirstName, e.MiddleName, e.LastName, e.CompanyName,
+			e.CompanyKvk, e.CompanyType, e.Street, e.Number, e.PostalCode, e.City,
+			e.PhoneNumber, e.FaxNumber, e.Email, e.Country) + "\n"
+	}
+
+	return output + fmt.Sprintf("</%s>", key)
+}
diff --git a/vendor/github.com/transip/gotransip/sign.go b/vendor/github.com/transip/gotransip/sign.go
new file mode 100644
index 00000000..8b7d360c
--- /dev/null
+++ b/vendor/github.com/transip/gotransip/sign.go
@@ -0,0 +1,49 @@
+package gotransip
+
+import (
+	"crypto"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/sha512"
+	"crypto/x509"
+	"encoding/base64"
+	"encoding/pem"
+	"errors"
+	"fmt"
+	"net/url"
+)
+
+var (
+	asn1Header = []byte{
+		0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03,
+		0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40,
+	}
+)
+
+func signWithKey(params *soapParams, key []byte) (string, error) {
+	// create SHA512 hash of given parameters
+	h := sha512.New()
+	h.Write([]byte(params.Encode()))
+
+	// prefix ASN1 header to SHA512 hash
+	digest := append(asn1Header, h.Sum(nil)...)
+
+	// prepare key struct
+	block, _ := pem.Decode(key)
+	if block == nil {
+		return "", errors.New("could not decode private key")
+	}
+	parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+	if err != nil {
+		return "", fmt.Errorf("could not parse private key: %s", err.Error())
+	}
+
+	pkey := parsed.(*rsa.PrivateKey)
+
+	enc, err := rsa.SignPKCS1v15(rand.Reader, pkey, crypto.Hash(0), digest)
+	if err != nil {
+		return "", fmt.Errorf("could not sign data: %s", err.Error())
+	}
+
+	return url.QueryEscape(base64.StdEncoding.EncodeToString(enc)), nil
+}
diff --git a/vendor/github.com/transip/gotransip/soap.go b/vendor/github.com/transip/gotransip/soap.go
new file mode 100644
index 00000000..b893b7f9
--- /dev/null
+++ b/vendor/github.com/transip/gotransip/soap.go
@@ -0,0 +1,419 @@
+package gotransip
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/xml"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"math/rand"
+	"net/http"
+	"net/url"
+	"regexp"
+	"strings"
+	"time"
+)
+
+const (
+	// format for SOAP envelopes
+	soapEnvelopeFixture string = `<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="%s">
+	<SOAP-ENV:Body>%s</SOAP-ENV:Body>
+</SOAP-ENV:Envelope>`
+)
+
+// getSOAPArgs returns XML representing given name and argument as SOAP body
+func getSOAPArgs(name string, input ...string) []byte {
+	var buf bytes.Buffer
+
+	buf.WriteString(fmt.Sprintf("<ns1:%s>", name))
+	for _, x := range input {
+		buf.WriteString(x)
+	}
+	buf.WriteString(fmt.Sprintf("</ns1:%s>", name))
+
+	return buf.Bytes()
+}
+
+// getSOAPArg returns XML representing given input argument as SOAP parameters
+// in combination with getSOAPArgs you can build SOAP body
+func getSOAPArg(name string, input interface{}) (output string) {
+	switch input.(type) {
+	case []string:
+		i := input.([]string)
+		output = fmt.Sprintf(`<%s SOAP-ENC:arrayType="xsd:string[%d]" xsi:type="ns1:ArrayOfString">`, name, len(i))
+		for _, x := range i {
+			output = output + fmt.Sprintf(`<item xsi:type="xsd:string">%s</item>`, x)
+		}
+		output = output + fmt.Sprintf("</%s>", name)
+	case string:
+		output = fmt.Sprintf(`<%s xsi:type="xsd:string">%s</%s>`, name, input, name)
+	case int, int32, int64:
+		output = fmt.Sprintf(`<%s xsi:type="xsd:integer">%d</%s>`, name, input, name)
+	}
+
+	return
+}
+
+type soapFault struct {
+	Code        string `xml:"faultcode,omitempty"`
+	Description string `xml:"faultstring,omitempty"`
+}
+
+func (s soapFault) String() string {
+	return fmt.Sprintf("SOAP Fault %s: %s", s.Code, s.Description)
+}
+
+// paramsEncoder allows SoapParams to hook into encoding theirselves, useful when
+// fields consist of complex structs
+type paramsEncoder interface {
+	EncodeParams(ParamsContainer)
+	EncodeArgs(string) string
+}
+
+// ParamsContainer is the interface a type should implement to be able to hold
+// SOAP parameters
+type ParamsContainer interface {
+	Len() int
+	Add(string, interface{})
+}
+
+// soapParams is a utility to make sure parameter data is encoded into a query
+// in the same order as we set them. The TransIP API requires this order for
+// verifying the signature
+type soapParams struct {
+	keys   []string
+	values []interface{}
+}
+
+// Add adds parameter data to the end of this SoapParams
+func (s *soapParams) Add(k string, v interface{}) {
+	if s.keys == nil {
+		s.keys = make([]string, 0)
+	}
+
+	if s.values == nil {
+		s.values = make([]interface{}, 0)
+	}
+
+	s.keys = append(s.keys, k)
+	s.values = append(s.values, v)
+}
+
+// Len returns amount of parameters set in this SoapParams
+func (s soapParams) Len() int {
+	return len(s.keys)
+}
+
+// Encode returns a URL-like query string that can be used to generate a request's
+// signature. It's similar to url.Values.Encode() but without sorting of the keys
+// and based on the value's type it tries to encode accordingly.
+func (s soapParams) Encode() string {
+	var buf bytes.Buffer
+	var key string
+
+	for i, v := range s.values {
+		// if this is not the first parameter, prefix with &
+		if i > 0 {
+			buf.WriteString("&")
+		}
+
+		// for empty data fields, don't encode anything
+		if v == nil {
+			continue
+		}
+
+		key = s.keys[i]
+
+		switch v.(type) {
+		case []string:
+			c := v.([]string)
+			for j, cc := range c {
+				if j > 0 {
+					buf.WriteString("&")
+				}
+				buf.WriteString(fmt.Sprintf("%s[%d]=", key, j))
+				buf.WriteString(strings.Replace(url.QueryEscape(cc), "+", "%20", -1))
+			}
+		case string:
+			c := v.(string)
+			buf.WriteString(fmt.Sprintf("%s=", key))
+			buf.WriteString(strings.Replace(url.QueryEscape(c), "+", "%20", -1))
+		case int, int8, int16, int32, int64:
+			buf.WriteString(fmt.Sprintf("%s=", key))
+			buf.WriteString(fmt.Sprintf("%d", v))
+		default:
+			continue
+		}
+	}
+
+	return buf.String()
+}
+
+type soapHeader struct {
+	XMLName  struct{} `xml:"Header"`
+	Contents []byte   `xml:",innerxml"`
+}
+
+type soapBody struct {
+	XMLName  struct{} `xml:"Body"`
+	Contents []byte   `xml:",innerxml"`
+}
+
+type soapResponse struct {
+	Response struct {
+		InnerXML []byte `xml:",innerxml"`
+	} `xml:"return"`
+}
+
+type soapEnvelope struct {
+	XMLName struct{} `xml:"Envelope"`
+	Header  soapHeader
+	Body    soapBody
+}
+
+// SoapRequest holds all information for perfoming a SOAP request
+// Arguments to the request can be specified with AddArgument
+// If padding is defined, the SOAP response will be parsed after it being padded
+// with items in Padding in reverse order
+type SoapRequest struct {
+	Service string
+	Method  string
+	params  *soapParams // params used for creating signature
+	args    []string    // XML body arguments
+	Padding []string
+}
+
+// AddArgument adds an argument to the SoapRequest; the arguments ared used to
+// fill the XML request body as well as to create a valid signature for the
+// request
+func (sr *SoapRequest) AddArgument(key string, value interface{}) {
+	if sr.params == nil {
+		sr.params = &soapParams{}
+	}
+
+	// check if value implements paramsEncoder
+	if pe, ok := value.(paramsEncoder); ok {
+		sr.args = append(sr.args, pe.EncodeArgs(key))
+		pe.EncodeParams(sr.params)
+		return
+	}
+
+	switch value.(type) {
+	case []string:
+		sr.params.Add(fmt.Sprintf("%d", sr.params.Len()), value)
+		sr.args = append(sr.args, getSOAPArg(key, value))
+	case string:
+		sr.params.Add(fmt.Sprintf("%d", sr.params.Len()), value)
+		sr.args = append(sr.args, getSOAPArg(key, value.(string)))
+	case int, int8, int16, int32, int64:
+		sr.params.Add(fmt.Sprintf("%d", sr.params.Len()), value)
+		sr.args = append(sr.args, getSOAPArg(key, fmt.Sprintf("%d", value)))
+	default:
+		// check if value implements the String interface
+		if str, ok := value.(fmt.Stringer); ok {
+			sr.params.Add(fmt.Sprintf("%d", sr.params.Len()), str.String())
+			sr.args = append(sr.args, getSOAPArg(key, str.String()))
+		}
+	}
+}
+
+func (sr SoapRequest) getEnvelope() string {
+	return fmt.Sprintf(soapEnvelopeFixture, transipAPIHost, getSOAPArgs(sr.Method, sr.args...))
+}
+
+type soapClient struct {
+	Login      string
+	Mode       APIMode
+	PrivateKey []byte
+}
+
+// httpReqForSoapRequest creates the HTTP request for a specific SoapRequest
+// this includes setting the URL, POST body and cookies
+func (s soapClient) httpReqForSoapRequest(req SoapRequest) (*http.Request, error) {
+	// format URL
+	url := fmt.Sprintf("https://%s/soap/?service=%s", transipAPIHost, req.Service)
+
+	// create HTTP request
+	// TransIP API SOAP requests are always POST requests
+	httpReq, err := http.NewRequest("POST", url, strings.NewReader(req.getEnvelope()))
+	if err != nil {
+		return nil, err
+	}
+
+	// generate a number-used-once, a.k.a. nonce
+	// seeding the RNG is important if we want to do prevent using the same nonce
+	// in 2 sequential requests
+	rand.Seed(time.Now().UnixNano())
+	nonce := fmt.Sprintf("%d", rand.Int())
+	// set time of request, used later for signature as well
+	timestamp := fmt.Sprintf("%d", time.Now().Unix())
+
+	// set cookies required for the request
+	// most of these cookies are used for the signature as well so they should
+	// obviously match
+	httpReq.AddCookie(&http.Cookie{
+		Name:  "login",
+		Value: s.Login,
+	})
+	httpReq.AddCookie(&http.Cookie{
+		Name:  "mode",
+		Value: string(s.Mode),
+	})
+	httpReq.AddCookie(&http.Cookie{
+		Name:  "timestamp",
+		Value: timestamp,
+	})
+	httpReq.AddCookie(&http.Cookie{
+		Name:  "nonce",
+		Value: nonce,
+	})
+
+	// add params required for signature to the request parameters
+	if req.params == nil {
+		req.params = &soapParams{}
+	}
+	// TransIP API is quite picky on the order of the parameters
+	// so don't change anything in the order below
+	req.params.Add("__method", req.Method)
+	req.params.Add("__service", req.Service)
+	req.params.Add("__hostname", transipAPIHost)
+	req.params.Add("__timestamp", timestamp)
+	req.params.Add("__nonce", nonce)
+
+	signature, err := signWithKey(req.params, s.PrivateKey)
+	if err != nil {
+		return nil, err
+	}
+
+	// add signature of the request to the cookies as well
+	httpReq.AddCookie(&http.Cookie{
+		Name:  "signature",
+		Value: signature,
+	})
+
+	return httpReq, nil
+}
+
+func parseSoapResponse(data []byte, padding []string, statusCode int, result interface{}) error {
+	// try to decode the resulting XML
+	var env soapEnvelope
+	if err := xml.Unmarshal(data, &env); err != nil {
+		return err
+	}
+
+	// try to decode the body to a soapFault
+	var fault soapFault
+	if err := xml.Unmarshal(env.Body.Contents, &fault); err != nil {
+		return err
+	}
+
+	// by checking fault's Code, we can determine if the response body in fact
+	// was a SOAP fault and if it was: return it as an error
+	if len(fault.Code) > 0 {
+		return errors.New(fault.String())
+	}
+
+	// try to decode into soapResponse
+	sr := soapResponse{}
+	if err := xml.Unmarshal(env.Body.Contents, &sr); err != nil {
+		return err
+	}
+
+	// if the response was empty and HTTP status was 200, consider it a success
+	if len(sr.Response.InnerXML) == 0 && statusCode == 200 {
+		return nil
+	}
+
+	// it seems like xml.Unmarshal won't work well on the most outer element
+	// so even when no Padding is defined, always pad with "transip" element
+	p := append([]string{"transip"}, padding...)
+	innerXML := padXMLData(sr.Response.InnerXML, p)
+
+	// try to decode to given result interface
+	return xml.Unmarshal([]byte(innerXML), &result)
+}
+
+func (s *soapClient) call(req SoapRequest, result interface{}) error {
+	// get http request for soap request
+	httpReq, err := s.httpReqForSoapRequest(req)
+	if err != nil {
+		return err
+	}
+
+	// create HTTP client and do the actual request
+	client := &http.Client{Timeout: time.Second * 10}
+	// make sure to verify the validity of remote certificate
+	// this is the default, but adding this flag here makes it easier to toggle
+	// it for testing/debugging
+	client.Transport = &http.Transport{
+		TLSClientConfig: &tls.Config{
+			InsecureSkipVerify: false,
+		},
+	}
+	resp, err := client.Do(httpReq)
+	if err != nil {
+		return fmt.Errorf("request error:\n%s", err.Error())
+	}
+	defer resp.Body.Close()
+
+	// read entire response body
+	b, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return err
+	}
+
+	// parse SOAP response into given result interface
+	return parseSoapResponse(b, req.Padding, resp.StatusCode, result)
+}
+
+// apply given padding around the XML data fed into this function
+// padding is applied in reverse order, so last element of padding is the
+// innermost element in the resulting XML
+func padXMLData(data []byte, padding []string) []byte {
+	// get right information from padding elements by matching to regex
+	re, _ := regexp.Compile("^<?(?:([^ ]+) )?([^>]+)>?$")
+
+	var prefix, suffix []byte
+	var tag, attr string
+	// go over each padding element
+	for i := len(padding); i > 0; i-- {
+		res := re.FindStringSubmatch(padding[i-1])
+		// no attribute was given
+		if len(res[1]) == 0 {
+			tag = res[2]
+			attr = ""
+		} else {
+			tag = res[1]
+			attr = " " + res[2]
+		}
+
+		prefix = []byte(fmt.Sprintf("<%s%s>", tag, attr))
+		suffix = []byte(fmt.Sprintf("</%s>", tag))
+		data = append(append(prefix, data...), suffix...)
+	}
+
+	return data
+}
+
+// TestParamsContainer is only useful for unit testing the ParamsContainer
+// implementation of other type
+type TestParamsContainer struct {
+	Prm string
+}
+
+// Add just makes sure we use Len(), key and value in the result so it can be
+// tested
+func (t *TestParamsContainer) Add(key string, value interface{}) {
+	var prefix string
+	if t.Len() > 0 {
+		prefix = "&"
+	}
+	t.Prm = t.Prm + prefix + fmt.Sprintf("%d%s=%s", t.Len(), key, value)
+}
+
+// Len returns current length of test data in TestParamsContainer
+func (t TestParamsContainer) Len() int {
+	return len(t.Prm)
+}
diff --git a/vendor/github.com/transip/gotransip/util/util.go b/vendor/github.com/transip/gotransip/util/util.go
new file mode 100644
index 00000000..7f1beecf
--- /dev/null
+++ b/vendor/github.com/transip/gotransip/util/util.go
@@ -0,0 +1,37 @@
+package util
+
+import (
+	"encoding/xml"
+	"time"
+)
+
+// KeyValueXML resembles the complex struct for getting key/value pairs from XML
+type KeyValueXML struct {
+	Cont []struct {
+		Item []struct {
+			Key   string `xml:"key"`
+			Value string `xml:"value"`
+		} `xml:"item"`
+	} `xml:"item"`
+}
+
+// XMLTime is a custom type to decode XML values to time.Time directly
+type XMLTime struct {
+	time.Time
+}
+
+// UnmarshalXML is implemented to be able act as custom XML type
+// it tries to parse time from given elements value
+func (x *XMLTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+	var v string
+	if err := d.DecodeElement(&v, &start); err != nil {
+		return err
+	}
+
+	if p, _ := time.Parse("2006-01-02 15:04:05", v); !p.IsZero() {
+		*x = XMLTime{p}
+	} else if p, _ := time.Parse("2006-01-02", v); !p.IsZero() {
+		*x = XMLTime{p}
+	}
+	return nil
+}


Test patch:
diff --git a/providers/dns/transip/transip_test.go b/providers/dns/transip/transip_test.go
new file mode 100644
index 00000000..35c622ca
--- /dev/null
+++ b/providers/dns/transip/transip_test.go
@@ -0,0 +1,164 @@
+package transip
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+	"github.com/xenolf/lego/platform/tester"
+)
+
+var envTest = tester.NewEnvTest(
+	"TRANSIP_ACCOUNT_NAME",
+	"TRANSIP_PRIVATE_KEY_PATH").
+	WithDomain("TRANSIP_DOMAIN")
+
+func TestNewDNSProvider(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		envVars  map[string]string
+		expected string
+	}{
+		{
+			desc: "success",
+			envVars: map[string]string{
+				"TRANSIP_ACCOUNT_NAME":     "johndoe",
+				"TRANSIP_PRIVATE_KEY_PATH": "./fixtures/private.key",
+			},
+		},
+		{
+			desc: "missing all credentials",
+			envVars: map[string]string{
+				"TRANSIP_ACCOUNT_NAME":     "",
+				"TRANSIP_PRIVATE_KEY_PATH": "",
+			},
+			expected: "transip: some credentials information are missing: TRANSIP_ACCOUNT_NAME,TRANSIP_PRIVATE_KEY_PATH",
+		},
+		{
+			desc: "missing account name",
+			envVars: map[string]string{
+				"TRANSIP_ACCOUNT_NAME":     "",
+				"TRANSIP_PRIVATE_KEY_PATH": "./fixtures/private.key",
+			},
+			expected: "transip: some credentials information are missing: TRANSIP_ACCOUNT_NAME",
+		},
+		{
+			desc: "missing private key path",
+			envVars: map[string]string{
+				"TRANSIP_ACCOUNT_NAME":     "johndoe",
+				"TRANSIP_PRIVATE_KEY_PATH": "",
+			},
+			expected: "transip: some credentials information are missing: TRANSIP_PRIVATE_KEY_PATH",
+		},
+		{
+			desc: "could not open private key path",
+			envVars: map[string]string{
+				"TRANSIP_ACCOUNT_NAME":     "johndoe",
+				"TRANSIP_PRIVATE_KEY_PATH": "./fixtures/non/existent/private.key",
+			},
+			expected: "transip: could not open private key: stat ./fixtures/non/existent/private.key: no such file or directory",
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			defer envTest.RestoreEnv()
+			envTest.ClearEnv()
+
+			envTest.Apply(test.envVars)
+
+			p, err := NewDNSProvider()
+
+			if len(test.expected) == 0 {
+				require.NoError(t, err)
+				require.NotNil(t, p)
+				require.NotNil(t, p.config)
+				require.NotNil(t, p.client)
+			} else {
+				require.EqualError(t, err, test.expected)
+			}
+		})
+	}
+}
+
+func TestNewDNSProviderConfig(t *testing.T) {
+	testCases := []struct {
+		desc           string
+		accountName    string
+		privateKeyPath string
+		expected       string
+	}{
+		{
+			desc:           "success",
+			accountName:    "johndoe",
+			privateKeyPath: "./fixtures/private.key",
+		},
+		{
+			desc:     "missing all credentials",
+			expected: "transip: AccountName is required",
+		},
+		{
+			desc:           "missing account name",
+			privateKeyPath: "./fixtures/private.key",
+			expected:       "transip: AccountName is required",
+		},
+		{
+			desc:        "missing private key path",
+			accountName: "johndoe",
+			expected:    "transip: PrivateKeyPath or PrivateKeyBody is required",
+		},
+		{
+			desc:           "could not open private key path",
+			accountName:    "johndoe",
+			privateKeyPath: "./fixtures/non/existent/private.key",
+			expected:       "transip: could not open private key: stat ./fixtures/non/existent/private.key: no such file or directory",
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			config := NewDefaultConfig()
+			config.AccountName = test.accountName
+			config.PrivateKeyPath = test.privateKeyPath
+
+			p, err := NewDNSProviderConfig(config)
+
+			if len(test.expected) == 0 {
+				require.NoError(t, err)
+				require.NotNil(t, p)
+				require.NotNil(t, p.config)
+				require.NotNil(t, p.client)
+			} else {
+				require.EqualError(t, err, test.expected)
+			}
+		})
+	}
+}
+
+func TestLivePresent(t *testing.T) {
+	if !envTest.IsLiveTest() {
+		t.Skip("skipping live test")
+	}
+
+	envTest.RestoreEnv()
+	provider, err := NewDNSProvider()
+	require.NoError(t, err)
+
+	err = provider.Present(envTest.GetDomain(), "", "123d==")
+	require.NoError(t, err)
+}
+
+func TestLiveCleanUp(t *testing.T) {
+	if !envTest.IsLiveTest() {
+		t.Skip("skipping live test")
+	}
+
+	envTest.RestoreEnv()
+	provider, err := NewDNSProvider()
+	require.NoError(t, err)
+
+	time.Sleep(1 * time.Second)
+
+	err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
+	require.NoError(t, err)
+}