pax_global_header00006660000000000000000000000064137006342540014516gustar00rootroot0000000000000052 comment=302dfc012714871f55e4ce3bae69f903fe28327b go-ovh-1.1.0/000077500000000000000000000000001370063425400127145ustar00rootroot00000000000000go-ovh-1.1.0/.gitignore000066400000000000000000000000401370063425400146760ustar00rootroot00000000000000# Temporary edit files *.swp *~ go-ovh-1.1.0/.travis.yml000066400000000000000000000017611370063425400150320ustar00rootroot00000000000000language: go cache: directories: - $GOPATH/pkg/mod jobs: fast_finish: true include: - go: 1.12.x - go: 1.13.x - go: 1.14.x - go: 1.x - go: tip allow_failures: - go: tip before_install: - export GO111MODULE=off - go get github.com/axw/gocov/gocov - go get github.com/mattn/goveralls - go get golang.org/x/lint/golint - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi - GO111MODULE=on install: - go mod tidy - git diff --exit-code go.mod - git diff --exit-code go.sum - go mod download script: # Test Code quality - go vet ./... - ${GOPATH}/bin/golint ./... - go test -covermode=count -coverprofile=profile.cov ./... # Test buildable on most common platforms, beyond Linux - GOOS=darwin go build ./... - GOOS=windows go build ./... # Best effort: notify coveralls. It's too unstable, ignore errors. - $HOME/gopath/bin/goveralls -coverprofile=profile.cov -service=travis-ci || exit 0 go-ovh-1.1.0/CONTRIBUTING.md000066400000000000000000000067401370063425400151540ustar00rootroot00000000000000# Contributing to go-ovh ## Submitting Modifications: So you want to contribute you work? Awesome! We are eager to review it. To submit your contribution, you must use Github Pull Requests. Your work does not need to be fully polished before submiting it. Actully, we love helping people writing a great contribution. Hence, if you are wondering how to integrate a specific change, feel free to start a discussion in a Pull Request. Before we can actually accept and merge a Pull Request, it will need to follow the conding guidelines (see below), and each commit shall be signed to indicate your full agreement with these guidelines and the DCO (see below). To sign a commit, you may use a command like: ``` # New commit git commit -s # Previous commit git commit --amend -s ``` If a Pull Request can not be automatically merged, you will probably need to "rebase" your work on latest project update: ``` # Assuming, this project remote is registered as "upstream" git fetch upstream git rebase upstream/master ``` ## Contribution guidelines 1. your code must follow the coding style rules (see below) 2. your code must be documented 3. you code must be tested 4. your work must be signed (see "Developer Certificate of Origin" below) 5. you may contribute through GitHub Pull Requests ## Coding and documentation Style: - Code must be formated with `gofmt -sw ./` - Code must pass `go vet ./...` - Code must pass `golint ./...` ## Licensing for new files: go-ovh is licensed under a (modified) BSD license. Anything contributed to go-ovh must be released under this license. When introducing a new file into the project, please make sure it has a copyright header making clear under which license it''s being released. ## Developer Certificate of Origin: ``` To improve tracking of contributions to this project we will use a process modeled on the modified DCO 1.1 and use a "sign-off" procedure on patches that are being contributed. The sign-off is a simple line at the end of the explanation for the patch, which certifies that you wrote it or otherwise have the right to pass it on as an open-source patch. The rules are pretty simple: if you can certify the below: By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source License and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) The contribution is made free of any other party''s intellectual property claims or rights. (e) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. then you just add a line saying Signed-off-by: Random J Developer using your real name (sorry, no pseudonyms or anonymous contributions.) ``` go-ovh-1.1.0/LICENSE000066400000000000000000000026751370063425400137330ustar00rootroot00000000000000Copyright (c) 2015-2017, OVH SAS. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of OVH SAS nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. go-ovh-1.1.0/README.md000066400000000000000000000353421370063425400142020ustar00rootroot00000000000000go-ovh ====== Lightweight Go wrapper around OVH's APIs. Handles all the hard work including credential creation and requests signing. [![GoDoc](https://godoc.org/github.com/ovh/go-ovh/go-ovh?status.svg)](http://godoc.org/github.com/ovh/go-ovh/ovh) [![Build Status](https://travis-ci.org/ovh/go-ovh.svg?branch=master)](https://travis-ci.org/ovh/go-ovh) [![Coverage Status](https://coveralls.io/repos/github/ovh/go-ovh/badge.svg?branch=master)](https://coveralls.io/github/ovh/go-ovh?branch=master) [![Go Report Card](https://goreportcard.com/badge/ovh/go-ovh)](http://goreportcard.com/report/ovh/go-ovh) ```go package main import ( "fmt" "github.com/ovh/go-ovh/ovh" ) // PartialMe holds the first name of the currently logged-in user. // Visit https://api.ovh.com/console/#/me#GET for the full definition type PartialMe struct { Firstname string `json:"firstname"` } // Instantiate an OVH client and get the firstname of the currently logged-in user. // Visit https://api.ovh.com/createToken/index.cgi?GET=/me to get your credentials. func main() { var me PartialMe client, _ := ovh.NewClient( "ovh-eu", YOUR_APPLICATION_KEY, YOUR_APPLICATION_SECRET, YOUR_CONSUMER_KEY, ) client.Get("/me", &me) fmt.Printf("Welcome %s!\n", me.Firstname) } ``` ## Installation The Golang wrapper has been tested with Golang 1.5+. It may worker with older versions although it has not been tested. To use it, just include it to your ``import`` and run ``go get``: ```go import ( ... "github.com/ovh/go-ovh/ovh" ) ``` ## Configuration The straightforward way to use OVH's API keys is to embed them directly in the application code. While this is very convenient, it lacks of elegance and flexibility. Alternatively it is suggested to use configuration files or environment variables so that the same code may run seamlessly in multiple environments. Production and development for instance. This wrapper will first look for direct instanciation parameters then ``OVH_ENDPOINT``, ``OVH_APPLICATION_KEY``, ``OVH_APPLICATION_SECRET`` and ``OVH_CONSUMER_KEY`` environment variables. If either of these parameter is not provided, it will look for a configuration file of the form: ```ini [default] ; general configuration: default endpoint endpoint=ovh-eu [ovh-eu] ; configuration specific to 'ovh-eu' endpoint application_key=my_app_key application_secret=my_application_secret consumer_key=my_consumer_key ``` Depending on the API you want to use, you may set the ``endpoint`` to: * ``ovh-eu`` for OVH Europe API * ``ovh-us`` for OVH US API * ``ovh-ca`` for OVH Canada API * ``soyoustart-eu`` for So you Start Europe API * ``soyoustart-ca`` for So you Start Canada API * ``kimsufi-eu`` for Kimsufi Europe API * ``kimsufi-ca`` for Kimsufi Canada API * Or any arbitrary URL to use in a test for example The client will successively attempt to locate this configuration file in 1. Current working directory: ``./ovh.conf`` 2. Current user's home directory ``~/.ovh.conf`` 3. System wide configuration ``/etc/ovh.conf`` This lookup mechanism makes it easy to overload credentials for a specific project or user. ## Register your app OVH's API, like most modern APIs is designed to authenticate both an application and a user, without requiring the user to provide a password. Your application will be identified by its "application secret" and "application key" tokens. Hence, to use the API, you must first register your application and then ask your user to authenticate on a specific URL. Once authenticated, you'll have a valid "consumer key" which will grant your application on specific APIs. The user may choose the validity period of its authorization. The default period is 24h. He may also revoke an authorization at any time. Hence, your application should be prepared to receive 403 HTTP errors and prompt the user to re-authenticated. This process is detailed in the following section. Alternatively, you may only need to build an application for a single user. In this case you may generate all credentials at once. See below. ### Use the API on behalf of a user Visit [https://eu.api.ovh.com/createApp](https://eu.api.ovh.com/createApp) and create your app You'll get an application key and an application secret. To use the API you'll need a consumer key. The consumer key has two types of restriction: * path: eg. only the ```GET``` method on ```/me``` * time: eg. expire in 1 day Then, get a consumer key. Here's an example on how to generate one. First, create a 'ovh.conf' file in the current directory with the application key and application secret. You can add the consumer key once generated. For alternate configuration method, please see the [configuration section](#configuration). ```ini [ovh-eu] application_key=my_app_key application_secret=my_application_secret ; consumer_key=my_consumer_key ``` Then, you may use a program like this example to create a consumer key for the application: ```go package main import ( "fmt" "github.com/ovh/go-ovh/ovh" ) func main() { // Create a client using credentials from config files or environment variables client, err := ovh.NewEndpointClient("ovh-eu") if err != nil { fmt.Printf("Error: %q\n", err) return } ckReq := client.NewCkRequest() // Allow GET method on /me ckReq.AddRules(ovh.ReadOnly, "/me") // Allow GET method on /xdsl and all its sub routes ckReq.AddRecursiveRules(ovh.ReadOnly, "/xdsl") // Run the request response, err := ckReq.Do() if err != nil { fmt.Printf("Error: %q\n", err) return } // Print the validation URL and the Consumer key fmt.Printf("Generated consumer key: %s\n", response.ConsumerKey) fmt.Printf("Please visit %s to validate it\n", response.ValidationURL) } ``` ### Use the API for a single user Alternatively, you may generate all creadentials at once, including the consumer key. You will typically want to do this when writing automation scripts for a single projects. If this case, you may want to directly go to https://eu.api.ovh.com/createToken/ to generate the 3 tokens at once. Make sure to save them in one of the 'ovh.conf' configuration file. Please see the [configuration section](#configuration). ``ovh.conf`` should look like: ```ini [ovh-eu] application_key=my_app_key application_secret=my_application_secret consumer_key=my_consumer_key ``` ## Use the lib These examples assume valid credentials are available in the [configuration](#configuration). ### GET ```go package main import ( "fmt" "github.com/ovh/go-ovh/ovh" ) func main() { client, err := ovh.NewEndpointClient("ovh-eu") if err != nil { fmt.Printf("Error: %q\n", err) return } // Get all the xdsl services xdslServices := []string{} if err := client.Get("/xdsl/", &xdslServices); err != nil { fmt.Printf("Error: %q\n", err) return } // xdslAccess represents a xdsl access returned by the API type xdslAccess struct { Name string `json:"accessName"` Status string `json:"status"` Pairs int `json:"pairsNumber"` // Insert the other properties here } // Get the details of each service for i, serviceName := range xdslServices { access := xdslAccess{} url := "/xdsl/" + serviceName if err := client.Get(url, &access); err != nil { fmt.Printf("Error: %q\n", err) return } fmt.Printf("#%d : %+v\n", i+1, access) } } ``` ### PUT ```go package main import ( "fmt" "github.com/ovh/go-ovh/ovh" ) func main() { client, err := ovh.NewEndpointClient("ovh-eu") if err != nil { fmt.Printf("Error: %q\n", err) return } // Params type AccessPutParams struct { Description string `json:"description"` } // Update the description of the service params := &AccessPutParams{Description: "My awesome access"} if err := client.Put("/xdsl/xdsl-yourservice", params, nil); err != nil { fmt.Printf("Error: %q\n", err) return } fmt.Println("Description updated") } ``` ## API Documentation ### Create a client - Use ``ovh.NewClient()`` to have full controll over ther authentication - Use ``ovh.NewEndpointClient()`` to create a client for a specific API and use credentials from config files or environment - Use ``ovh.NewDefaultClient()`` to create a client unsing endpoint and credentials from config files or environment ### Query Each HTTP verb has its own Client method. Some API methods supports unauthenticated calls. For these methods, you may want to use the ``*UnAuth`` variant of the Client which will bypass request signature. Each helper accepts a ``method`` and ``resType`` argument. ``method`` is the full URI, including the query string, and ``resType`` is a reference to an object in which the json response will be unserialized. Additionally, ``Post``, ``Put`` and their ``UnAuth`` variant accept a reqBody which is a reference to a json serializable object or nil. Alternatively, you may directly use the low level ``CallAPI`` method. - Use ``client.Get()`` for GET requests - Use ``client.Post()`` for POST requests - Use ``client.Put()`` for PUT requests - Use ``client.Delete()`` for DELETE requests Or, for unautenticated requests: - Use ``client.GetUnAuth()`` for GET requests - Use ``client.PostUnAuth()`` for POST requests - Use ``client.PutUnAuth()`` for PUT requests - Use ``client.DeleteUnAuth()`` for DELETE requests ### Request consumer keys Consumer keys may be restricted to a subset of the API. This allows to delegate the API to manage only a specific server or domain name for example. This is called "scoping" a consumer key. Rules are simple. They combine an HTTP verb (GET, POST, PUT or DELETE) with a pattern. A pattern is a plain API method and may contain the '*' wilcard to match "anything". Just like glob on a Unix machine. While this is simple and may be managed directly with the API as-is, this can be cumbersome to do and we recommend using the ``CkRequest`` helper. It basically manages the list of authorizations for you and the actual request. *example*: Grant on all /sms and identity ```go client, err := ovh.NewEndpointClient("ovh-eu") if err == nil { // Do something } req := client.NewCkRequest() req.AddRules(ovh.ReadOnly, "/me") req.AddRecursiveRulesRules(ovh.ReadWrite, "/sms") pendingCk, err := req.Do() ``` This example will generate a request for: - GET /me - GET /sms - GET /sms/* - POST /sms - POST /sms/* - PUT /sms - PUT /sms/* - DELETE /sms - DELETE /sms/* Which would be tedious to do by hand... *Create a ``CkRequest``*: ```go req := client.NewCkRequest() ``` *Request access on a specific path and method* (advanced): ```go // Use this method for fine-grain access control. In most case, you'll // want to use the methods below. req.AddRule("VERB", "PATTERN") ``` *Request access on specific path*: ```go // This will generate all patterns for GET PATH req.AddRules(ovh.ReadOnly, "/PATH") // This will generate all patterns for PATH for all HTTP verbs req.AddRules(ovh.ReadWrite, "/PATH") // This will generate all patterns for PATH for all HTTP verbs, except DELETE req.AddRules(ovh.ReadWriteSafe, "/PATH") ``` *Request access on path and all sub-path*: ```go // This will generate all patterns for GET PATH req.AddRecursiveRules(ovh.ReadOnly, "/PATH") // This will generate all patterns for PATH for all HTTP verbs req.AddRecursiveRules(ovh.ReadWrite, "/PATH") // This will generate all patterns for PATH for all HTTP verbs, except DELETE req.AddRecusriveRules(ovh.ReadWriteSafe, "/PATH") ``` *Create key*: ```go pendingCk, err := req.Do() ``` This will initiate the consumer key validation process and return both a consumer key and a validation URL. The consumer key is automatically added to the client which was used to create the request. It may be used as soon as the user has authenticated the request on the validation URL. ``pendingCk`` contains 3 fields: - ``ValidationURL`` the URL the user needs to visit to activate the consumer key - ``ConsumerKey`` the new consumer key. It won't be active until validation - ``State`` the consumer key state. Always "pendingValidation" at this stage ## Hacking This wrapper uses standard Go tools, so you should feel at home with it. Here is a quick outline of what it may look like. ### Get the sources ``` go get github.com/ovh/go-ovh/ovh cd $GOPATH/src/github.com/ovh/go-ovh/ovh go get ``` You've developed a new cool feature ? Fixed an annoying bug ? We'd be happy to hear from you ! See [CONTRIBUTING.md](https://github.com/ovh/go-ovh/blob/master/CONTRIBUTING.md) for more informations ### Run the tests Simply run ``go test``. Since we all love quality, please note that we do not accept contributions lowering coverage. ``` # Run all tests, with coverage go test -cover # Validate code quality golint ./... go vet ./... ``` ## Supported APIs ### OVH Europe - **Documentation**: https://eu.api.ovh.com/ - **Community support**: api-subscribe@ml.ovh.net - **Console**: https://eu.api.ovh.com/console - **Create application credentials**: https://eu.api.ovh.com/createApp/ - **Create script credentials** (all keys at once): https://eu.api.ovh.com/createToken/ ### OVH US - **Documentation**: https://api.us.ovhcloud.com/ - **Console**: https://api.us.ovhcloud.com/console/ - **Create application credentials**: https://api.us.ovhcloud.com/createApp/ - **Create script credentials** (all keys at once): https://api.us.ovhcloud.com/createToken/ ### OVH Canada - **Documentation**: https://ca.api.ovh.com/ - **Community support**: api-subscribe@ml.ovh.net - **Console**: https://ca.api.ovh.com/console - **Create application credentials**: https://ca.api.ovh.com/createApp/ - **Create script credentials** (all keys at once): https://ca.api.ovh.com/createToken/ ### So you Start Europe - **Documentation**: https://eu.api.soyoustart.com/ - **Community support**: api-subscribe@ml.ovh.net - **Console**: https://eu.api.soyoustart.com/console/ - **Create application credentials**: https://eu.api.soyoustart.com/createApp/ - **Create script credentials** (all keys at once): https://eu.api.soyoustart.com/createToken/ ### So you Start Canada - **Documentation**: https://ca.api.soyoustart.com/ - **Community support**: api-subscribe@ml.ovh.net - **Console**: https://ca.api.soyoustart.com/console/ - **Create application credentials**: https://ca.api.soyoustart.com/createApp/ - **Create script credentials** (all keys at once): https://ca.api.soyoustart.com/createToken/ ### Kimsufi Europe - **Documentation**: https://eu.api.kimsufi.com/ - **Community support**: api-subscribe@ml.ovh.net - **Console**: https://eu.api.kimsufi.com/console/ - **Create application credentials**: https://eu.api.kimsufi.com/createApp/ - **Create script credentials** (all keys at once): https://eu.api.kimsufi.com/createToken/ ### Kimsufi Canada - **Documentation**: https://ca.api.kimsufi.com/ - **Community support**: api-subscribe@ml.ovh.net - **Console**: https://ca.api.kimsufi.com/console/ - **Create application credentials**: https://ca.api.kimsufi.com/createApp/ - **Create script credentials** (all keys at once): https://ca.api.kimsufi.com/createToken/ ## License 3-Clause BSD go-ovh-1.1.0/go.mod000066400000000000000000000002601370063425400140200ustar00rootroot00000000000000module github.com/ovh/go-ovh go 1.12 require ( // required by gopkg.in/ini.v1 unit tests... github.com/smartystreets/goconvey v1.6.4 // indirect gopkg.in/ini.v1 v1.57.0 ) go-ovh-1.1.0/go.sum000066400000000000000000000031201370063425400140430ustar00rootroot00000000000000github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= go-ovh-1.1.0/ovh/000077500000000000000000000000001370063425400135105ustar00rootroot00000000000000go-ovh-1.1.0/ovh/configuration.go000066400000000000000000000074761370063425400167240ustar00rootroot00000000000000package ovh import ( "fmt" "os" "os/user" "path/filepath" "strings" "gopkg.in/ini.v1" ) // Use variables for easier test overload var ( systemConfigPath = "/etc/ovh.conf" userConfigPath = "/.ovh.conf" // prefixed with homeDir localConfigPath = "./ovh.conf" ) // currentUserHome attempts to get current user's home directory func currentUserHome() (string, error) { userHome := "" usr, err := user.Current() if err != nil { // Fallback by trying to read $HOME userHome = os.Getenv("HOME") if userHome != "" { err = nil } } else { userHome = usr.HomeDir } return userHome, nil } // appendConfigurationFile only if it exists. We need to do this because // ini package will fail to load configuration at all if a configuration // file is missing. This is racy, but better than always failing. func appendConfigurationFile(cfg *ini.File, path string) { if file, err := os.Open(path); err == nil { file.Close() cfg.Append(path) } } // loadConfig loads client configuration from params, environments or configuration // files (by order of decreasing precedence). // // loadConfig will check OVH_CONSUMER_KEY, OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET // and OVH_ENDPOINT environment variables. If any is present, it will take precedence // over any configuration from file. // // Configuration files are ini files. They share the same format as python-ovh, // node-ovh, php-ovh and all other wrappers. If any wrapper is configured, all // can re-use the same configuration. loadConfig will check for configuration in: // // - ./ovh.conf // - $HOME/.ovh.conf // - /etc/ovh.conf // func (c *Client) loadConfig(endpointName string) error { // Load configuration files by order of increasing priority. All configuration // files are optional. Only load file from user home if home could be resolve cfg := ini.Empty() appendConfigurationFile(cfg, systemConfigPath) if home, err := currentUserHome(); err == nil { userConfigFullPath := filepath.Join(home, userConfigPath) appendConfigurationFile(cfg, userConfigFullPath) } appendConfigurationFile(cfg, localConfigPath) // Canonicalize configuration if endpointName == "" { endpointName = getConfigValue(cfg, "default", "endpoint", "ovh-eu") } if c.AppKey == "" { c.AppKey = getConfigValue(cfg, endpointName, "application_key", "") } if c.AppSecret == "" { c.AppSecret = getConfigValue(cfg, endpointName, "application_secret", "") } if c.ConsumerKey == "" { c.ConsumerKey = getConfigValue(cfg, endpointName, "consumer_key", "") } // Load real endpoint URL by name. If endpoint contains a '/', consider it as a URL if strings.Contains(endpointName, "/") { c.endpoint = endpointName } else { c.endpoint = Endpoints[endpointName] } // If we still have no valid endpoint, AppKey or AppSecret, return an error if c.endpoint == "" { return fmt.Errorf("unknown endpoint '%s', consider checking 'Endpoints' list of using an URL", endpointName) } if c.AppKey == "" { return fmt.Errorf("missing application key, please check your configuration or consult the documentation to create one") } if c.AppSecret == "" { return fmt.Errorf("missing application secret, please check your configuration or consult the documentation to create one") } return nil } // getConfigValue returns the value of OVH_ or ``name`` value from ``section``. If // the value could not be read from either env or any configuration files, return 'def' func getConfigValue(cfg *ini.File, section, name, def string) string { // Attempt to load from environment fromEnv := os.Getenv("OVH_" + strings.ToUpper(name)) if len(fromEnv) > 0 { return fromEnv } // Attempt to load from configuration fromSection := cfg.Section(section) if fromSection == nil { return def } fromSectionKey := fromSection.Key(name) if fromSectionKey == nil { return def } return fromSectionKey.String() } go-ovh-1.1.0/ovh/configuration_test.go000066400000000000000000000143471370063425400177560ustar00rootroot00000000000000package ovh import ( "io/ioutil" "os" "testing" ) // // Utils // var home string func setup() { systemConfigPath = "./ovh.unittest.global.conf" userConfigPath = "/.ovh.unittest.user.conf" localConfigPath = "./ovh.unittest.local.conf" home, _ = currentUserHome() } func teardown() { os.Remove(systemConfigPath) os.Remove(home + userConfigPath) os.Remove(localConfigPath) } // // Tests // func TestConfigFromFiles(t *testing.T) { // Write each parameter to one different configuration file // This is a simple way to test precedence // Prepare ioutil.WriteFile(systemConfigPath, []byte(` [ovh-eu] application_key=system application_secret=system consumer_key=system `), 0660) ioutil.WriteFile(home+userConfigPath, []byte(` [ovh-eu] application_secret=user consumer_key=user `), 0660) ioutil.WriteFile(localConfigPath, []byte(` [ovh-eu] consumer_key=local `), 0660) // Clear defer ioutil.WriteFile(systemConfigPath, []byte(``), 0660) defer ioutil.WriteFile(home+userConfigPath, []byte(``), 0660) defer ioutil.WriteFile(localConfigPath, []byte(``), 0660) // Test client := Client{} err := client.loadConfig("ovh-eu") // Validate if err != nil { t.Fatalf("loadConfig failed with: '%v'", err) } if client.AppKey != "system" { t.Fatalf("client.AppKey should be 'system'. Got '%s'", client.AppKey) } if client.AppSecret != "user" { t.Fatalf("client.AppSecret should be 'user'. Got '%s'", client.AppSecret) } if client.ConsumerKey != "local" { t.Fatalf("client.ConsumerKey should be 'local'. Got '%s'", client.ConsumerKey) } } func TestConfigFromOnlyOneFile(t *testing.T) { // ini package has a bug causing it to ignore all subsequent configuration // files if any could not be loaded. Make sure that workaround... works. // Prepare os.Remove(systemConfigPath) ioutil.WriteFile(home+userConfigPath, []byte(` [ovh-eu] application_key=user application_secret=user consumer_key=user `), 0660) // Clear defer ioutil.WriteFile(home+userConfigPath, []byte(``), 0660) // Test client := Client{} err := client.loadConfig("ovh-eu") // Validate if err != nil { t.Fatalf("loadConfig failed with: '%v'", err) } if client.AppKey != "user" { t.Fatalf("client.AppKey should be 'user'. Got '%s'", client.AppKey) } if client.AppSecret != "user" { t.Fatalf("client.AppSecret should be 'user'. Got '%s'", client.AppSecret) } if client.ConsumerKey != "user" { t.Fatalf("client.ConsumerKey should be 'user'. Got '%s'", client.ConsumerKey) } } func TestConfigFromEnv(t *testing.T) { // Prepare ioutil.WriteFile(systemConfigPath, []byte(` [ovh-eu] application_key=fail application_secret=fail consumer_key=fail `), 0660) defer ioutil.WriteFile(systemConfigPath, []byte(``), 0660) os.Setenv("OVH_ENDPOINT", "ovh-eu") os.Setenv("OVH_APPLICATION_KEY", "env") os.Setenv("OVH_APPLICATION_SECRET", "env") os.Setenv("OVH_CONSUMER_KEY", "env") // Clear defer os.Unsetenv("OVH_ENDPOINT") defer os.Unsetenv("OVH_APPLICATION_KEY") defer os.Unsetenv("OVH_APPLICATION_SECRET") defer os.Unsetenv("OVH_CONSUMER_KEY") // Test client := Client{} err := client.loadConfig("") // Validate if err != nil { t.Fatalf("loadConfig failed with: '%v'", err) } if client.endpoint != OvhEU { t.Fatalf("client.AppKey should be 'env'. Got '%s'", client.AppKey) } if client.AppKey != "env" { t.Fatalf("client.AppKey should be 'env'. Got '%s'", client.AppKey) } if client.AppSecret != "env" { t.Fatalf("client.AppSecret should be 'env'. Got '%s'", client.AppSecret) } if client.ConsumerKey != "env" { t.Fatalf("client.ConsumerKey should be 'env'. Got '%s'", client.ConsumerKey) } } func TestConfigFromArgs(t *testing.T) { // Test client := Client{ AppKey: "param", AppSecret: "param", ConsumerKey: "param", } err := client.loadConfig("ovh-eu") // Validate if err != nil { t.Fatalf("loadConfig failed with: '%v'", err) } if client.endpoint != OvhEU { t.Fatalf("client.AppKey should be 'param'. Got '%s'", client.AppKey) } if client.AppKey != "param" { t.Fatalf("client.AppKey should be 'param'. Got '%s'", client.AppKey) } if client.AppSecret != "param" { t.Fatalf("client.AppSecret should be 'param'. Got '%s'", client.AppSecret) } if client.ConsumerKey != "param" { t.Fatalf("client.ConsumerKey should be 'param'. Got '%s'", client.ConsumerKey) } } func TestEndpoint(t *testing.T) { // Prepare ioutil.WriteFile(systemConfigPath, []byte(` [ovh-eu] application_key=ovh application_secret=ovh consumer_key=ovh [https://api.example.com:4242] application_key=example.com application_secret=example.com consumer_key=example.com `), 0660) // Clear defer ioutil.WriteFile(systemConfigPath, []byte(``), 0660) // Test: by name client := Client{} err := client.loadConfig("ovh-eu") if err != nil { t.Fatalf("loadConfig should not fail for endpoint 'ovh-eu'. Got '%v'", err) } if client.AppKey != "ovh" { t.Fatalf("configured value should be 'ovh' for endpoint 'ovh-eu'. Got '%s'", client.AppKey) } // Test: by URL client = Client{} err = client.loadConfig("https://api.example.com:4242") if err != nil { t.Fatalf("loadConfig should not fail for endpoint 'https://api.example.com:4242'. Got '%v'", err) } if client.AppKey != "example.com" { t.Fatalf("configured value should be 'example.com' for endpoint 'https://api.example.com:4242'. Got '%s'", client.AppKey) } } func TestMissingParam(t *testing.T) { // Setup var err error client := Client{ AppKey: "param", AppSecret: "param", ConsumerKey: "param", } // Test client.endpoint = "" if err = client.loadConfig(""); err == nil { t.Fatalf("loadConfig should fail when client.endpoint is missing. Got '%s'", client.endpoint) } client.AppKey = "" if err = client.loadConfig("ovh-eu"); err == nil { t.Fatalf("loadConfig should fail when client.AppKey is missing. Got '%s'", client.AppKey) } client.AppKey = "param" client.AppSecret = "" if err = client.loadConfig("ovh-eu"); err == nil { t.Fatalf("loadConfig should fail when client.AppSecret is missing. Got '%s'", client.AppSecret) } client.AppSecret = "param" } // // Main // // TestMain changes the location of configuration files. We need // this to avoid any interference with existing configuration // and non-root users func TestMain(m *testing.M) { setup() code := m.Run() teardown() os.Exit(code) } go-ovh-1.1.0/ovh/consumer_key.go000066400000000000000000000061661370063425400165530ustar00rootroot00000000000000package ovh import ( "fmt" "strings" ) // Map user friendly access level names to corresponding HTTP verbs var ( ReadOnly = []string{"GET"} ReadWrite = []string{"GET", "POST", "PUT", "DELETE"} ReadWriteSafe = []string{"GET", "POST", "PUT"} ) // AccessRule represents a method allowed for a path type AccessRule struct { // Allowed HTTP Method for the requested AccessRule. // Can be set to GET/POST/PUT/DELETE. Method string `json:"method"` // Allowed path. // Can be an exact string or a string with '*' char. // Example : // /me : only /me is authorized // /* : all calls are authorized Path string `json:"path"` } // CkValidationState represents the response when asking a new consumerKey. type CkValidationState struct { // Consumer key, which need to be validated by customer. ConsumerKey string `json:"consumerKey"` // Current status, should be always "pendingValidation". State string `json:"state"` // URL to redirect user in order to log in. ValidationURL string `json:"validationUrl"` } // CkRequest represents the parameters to fill in order to ask a new // consumerKey. type CkRequest struct { client *Client AccessRules []AccessRule `json:"accessRules"` Redirection string `json:"redirection,omitempty"` } func (ck *CkValidationState) String() string { return fmt.Sprintf("CK: %q\nStatus: %q\nValidation URL: %q\n", ck.ConsumerKey, ck.State, ck.ValidationURL, ) } // NewCkRequest helps create a new ck request func (c *Client) NewCkRequest() *CkRequest { return &CkRequest{ client: c, AccessRules: []AccessRule{}, } } // NewCkRequestWithRedirection helps create a new ck request with a redirect URL func (c *Client) NewCkRequestWithRedirection(redirection string) *CkRequest { return &CkRequest{ client: c, AccessRules: []AccessRule{}, Redirection: redirection, } } // AddRule adds a new rule to the ckRequest func (ck *CkRequest) AddRule(method, path string) { ck.AccessRules = append(ck.AccessRules, AccessRule{ Method: method, Path: path, }) } // AddRules adds grant requests on "path" for all methods. "ReadOnly", // "ReadWrite" and "ReadWriteSafe" should be used for "methods" unless // specific access are required. func (ck *CkRequest) AddRules(methods []string, path string) { for _, method := range methods { ck.AddRule(method, path) } } // AddRecursiveRules adds grant requests on "path" and "path/*", for all // methods "ReadOnly", "ReadWrite" and "ReadWriteSafe" should be used for // "methods" unless specific access are required. func (ck *CkRequest) AddRecursiveRules(methods []string, path string) { path = strings.TrimRight(path, "/") // Add rules. Skip base rule when requesting access to "/" if path != "" { ck.AddRules(methods, path) } ck.AddRules(methods, path+"/*") } // Do executes the request. On success, set the consumer key in the client // and return the URL the user needs to visit to validate the key func (ck *CkRequest) Do() (*CkValidationState, error) { state := CkValidationState{} err := ck.client.PostUnAuth("/auth/credential", ck, &state) if err == nil { ck.client.ConsumerKey = state.ConsumerKey } return &state, err } go-ovh-1.1.0/ovh/consumer_key_test.go000066400000000000000000000113321370063425400176010ustar00rootroot00000000000000package ovh import ( "fmt" "net/http" "reflect" "testing" "time" ) // Common helpers are in ovh_test.go func TestNewCkRequest(t *testing.T) { const expectedRequest = `{"accessRules":[{"method":"GET","path":"/me"},{"method":"GET","path":"/xdsl/*"}]}` // Init test var InputRequest *http.Request var InputRequestBody string ts, client := initMockServer(&InputRequest, 200, `{ "validationUrl":"https://validation.url", "ConsumerKey":"`+MockConsumerKey+`", "state":"pendingValidation" }`, &InputRequestBody, time.Duration(0)) client.ConsumerKey = "" defer ts.Close() // Test ckRequest := client.NewCkRequest() ckRequest.AddRule("GET", "/me") ckRequest.AddRule("GET", "/xdsl/*") got, err := ckRequest.Do() // Validate if err != nil { t.Fatalf("CkRequest.Do() should not return an error. Got: %q", err) } if client.ConsumerKey != MockConsumerKey { t.Fatalf("CkRequest.Do() should set client.ConsumerKey to %s. Got %s", MockConsumerKey, client.ConsumerKey) } if got.ConsumerKey != MockConsumerKey { t.Fatalf("CkRequest.Do() should set CkValidationState.ConsumerKey to %s. Got %s", MockConsumerKey, got.ConsumerKey) } if got.ValidationURL == "" { t.Fatalf("CkRequest.Do() should set CkValidationState.ValidationURL") } if InputRequestBody != expectedRequest { t.Fatalf("CkRequest.Do() should issue '%s' request. Got %s", expectedRequest, InputRequestBody) } ensureHeaderPresent(t, InputRequest, "Accept", "application/json") ensureHeaderPresent(t, InputRequest, "X-Ovh-Application", MockApplicationKey) } func TestInvalidCkRequest(t *testing.T) { // Init test var InputRequest *http.Request var InputRequestBody string ts, client := initMockServer(&InputRequest, http.StatusForbidden, `{"message":"Invalid application key"}`, &InputRequestBody, time.Duration(0)) client.ConsumerKey = "" defer ts.Close() // Test ckRequest := client.NewCkRequest() ckRequest.AddRule("GET", "/me") ckRequest.AddRule("GET", "/xdsl/*") _, err := ckRequest.Do() apiError, ok := err.(*APIError) // Validate if err == nil { t.Fatal("Expected an error, got none") } if !ok { t.Fatal("Expected error of type APIError") } if apiError.Code != http.StatusForbidden { t.Fatalf("Expected HTTP error 403. Got %d", apiError.Code) } if apiError.Message != "Invalid application key" { t.Fatalf("Expected API error message 'Invalid application key'. Got '%s'", apiError.Message) } } func TestAddRules(t *testing.T) { // Init test var InputRequest *http.Request var InputRequestBody string ts, client := initMockServer(&InputRequest, http.StatusForbidden, `{"message":"Invalid application key"}`, &InputRequestBody, time.Duration(0)) client.ConsumerKey = "" defer ts.Close() // Test: allow all ckRequest := client.NewCkRequest() ckRequest.AddRecursiveRules(ReadWrite, "/") ExpectedRules := []AccessRule{ {Method: "GET", Path: "/*"}, {Method: "POST", Path: "/*"}, {Method: "PUT", Path: "/*"}, {Method: "DELETE", Path: "/*"}, } if !reflect.DeepEqual(ckRequest.AccessRules, ExpectedRules) { t.Fatalf("Inserting recursive RW rules for / should generate %v. Got %v", ExpectedRules, ckRequest.AccessRules) } // Test: allow exactly /sms, RO ckRequest = client.NewCkRequest() ckRequest.AddRules(ReadOnly, "/sms") ExpectedRules = []AccessRule{ {Method: "GET", Path: "/sms"}, } if !reflect.DeepEqual(ckRequest.AccessRules, ExpectedRules) { t.Fatalf("Inserting RO rule for /sms should generate %v. Got %v", ExpectedRules, ckRequest.AccessRules) } // Test: allow /sms/*, RW, no delete ckRequest = client.NewCkRequest() ckRequest.AddRecursiveRules(ReadWriteSafe, "/sms") ExpectedRules = []AccessRule{ {Method: "GET", Path: "/sms"}, {Method: "POST", Path: "/sms"}, {Method: "PUT", Path: "/sms"}, {Method: "GET", Path: "/sms/*"}, {Method: "POST", Path: "/sms/*"}, {Method: "PUT", Path: "/sms/*"}, } if !reflect.DeepEqual(ckRequest.AccessRules, ExpectedRules) { t.Fatalf("Inserting recursive safe RW rule for /sms should generate %v. Got %v", ExpectedRules, ckRequest.AccessRules) } } func TestCkRequestString(t *testing.T) { ckValidationState := &CkValidationState{ ConsumerKey: "ck", State: "pending", ValidationURL: "fakeURL", } expected := fmt.Sprintf("CK: \"ck\"\nStatus: \"pending\"\nValidation URL: \"fakeURL\"\n") got := fmt.Sprintf("%s", ckValidationState) if got != expected { t.Errorf("expected %q, got %q", expected, got) } } func TestCkRequestRedirection(t *testing.T) { client, _ := NewClient("endpoint", "appKey", "appSecret", "consumerKey") redirection := "http://localhost/api/auth/callback?token=123456" ckRequest := client.NewCkRequestWithRedirection(redirection) if ckRequest.Redirection != redirection { t.Fatalf("NewCkRequestWithRedirection should set ckRequest.Redirection") } } go-ovh-1.1.0/ovh/error.go000066400000000000000000000005031370063425400151660ustar00rootroot00000000000000package ovh import "fmt" // APIError represents an error that can occurred while calling the API. type APIError struct { // Error message. Message string // HTTP code. Code int // ID of the request QueryID string } func (err *APIError) Error() string { return fmt.Sprintf("Error %d: %q", err.Code, err.Message) } go-ovh-1.1.0/ovh/error_test.go000066400000000000000000000005031370063425400162250ustar00rootroot00000000000000package ovh import ( "fmt" "net/http" "testing" ) func TestErrorString(t *testing.T) { err := &APIError{ Code: http.StatusBadRequest, Message: "Bad request", } expected := `Error 400: "Bad request"` got := fmt.Sprintf("%s", err) if got != expected { t.Errorf("expected %q, got %q", expected, got) } } go-ovh-1.1.0/ovh/logger.go000066400000000000000000000004741370063425400153230ustar00rootroot00000000000000package ovh import ( "net/http" ) // Logger is the interface that should be implemented for loggers that wish to // log HTTP requests and HTTP responses. type Logger interface { // LogRequest logs an HTTP request. LogRequest(*http.Request) // LogResponse logs an HTTP response. LogResponse(*http.Response) } go-ovh-1.1.0/ovh/ovh.go000066400000000000000000000313051370063425400146350ustar00rootroot00000000000000// Package ovh provides a HTTP wrapper for the OVH API. package ovh import ( "bytes" "context" "crypto/sha1" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "strconv" "sync" "time" ) // DefaultTimeout api requests after 180s const DefaultTimeout = 180 * time.Second // Endpoints const ( OvhEU = "https://eu.api.ovh.com/1.0" OvhCA = "https://ca.api.ovh.com/1.0" OvhUS = "https://api.us.ovhcloud.com/1.0" KimsufiEU = "https://eu.api.kimsufi.com/1.0" KimsufiCA = "https://ca.api.kimsufi.com/1.0" SoyoustartEU = "https://eu.api.soyoustart.com/1.0" SoyoustartCA = "https://ca.api.soyoustart.com/1.0" ) // Endpoints conveniently maps endpoints names to their URI for external configuration var Endpoints = map[string]string{ "ovh-eu": OvhEU, "ovh-ca": OvhCA, "ovh-us": OvhUS, "kimsufi-eu": KimsufiEU, "kimsufi-ca": KimsufiCA, "soyoustart-eu": SoyoustartEU, "soyoustart-ca": SoyoustartCA, } // Errors var ( ErrAPIDown = errors.New("go-vh: the OVH API is down, it does't respond to /time anymore") ) // Client represents a client to call the OVH API type Client struct { // Self generated tokens. Create one by visiting // https://eu.api.ovh.com/createApp/ // AppKey holds the Application key AppKey string // AppSecret holds the Application secret key AppSecret string // ConsumerKey holds the user/app specific token. It must have been validated before use. ConsumerKey string // API endpoint endpoint string // Client is the underlying HTTP client used to run the requests. It may be overloaded but a default one is instanciated in ``NewClient`` by default. Client *http.Client // Logger is used to log HTTP requests and responses. Logger Logger // Ensures that the timeDelta function is only ran once // sync.Once would consider init done, even in case of error // hence a good old flag timeDeltaMutex *sync.Mutex timeDeltaDone bool timeDelta time.Duration Timeout time.Duration } // NewClient represents a new client to call the API func NewClient(endpoint, appKey, appSecret, consumerKey string) (*Client, error) { client := Client{ AppKey: appKey, AppSecret: appSecret, ConsumerKey: consumerKey, Client: &http.Client{}, timeDeltaMutex: &sync.Mutex{}, timeDeltaDone: false, Timeout: time.Duration(DefaultTimeout), } // Get and check the configuration if err := client.loadConfig(endpoint); err != nil { return nil, err } return &client, nil } // NewEndpointClient will create an API client for specified // endpoint and load all credentials from environment or // configuration files func NewEndpointClient(endpoint string) (*Client, error) { return NewClient(endpoint, "", "", "") } // NewDefaultClient will load all it's parameter from environment // or configuration files func NewDefaultClient() (*Client, error) { return NewClient("", "", "", "") } // // High level helpers // // Ping performs a ping to OVH API. // In fact, ping is just a /auth/time call, in order to check if API is up. func (c *Client) Ping() error { _, err := c.getTime() return err } // TimeDelta represents the delay between the machine that runs the code and the // OVH API. The delay shouldn't change, let's do it only once. func (c *Client) TimeDelta() (time.Duration, error) { return c.getTimeDelta() } // Time returns time from the OVH API, by asking GET /auth/time. func (c *Client) Time() (*time.Time, error) { return c.getTime() } // // Common request wrappers // // Get is a wrapper for the GET method func (c *Client) Get(url string, resType interface{}) error { return c.CallAPI("GET", url, nil, resType, true) } // GetUnAuth is a wrapper for the unauthenticated GET method func (c *Client) GetUnAuth(url string, resType interface{}) error { return c.CallAPI("GET", url, nil, resType, false) } // Post is a wrapper for the POST method func (c *Client) Post(url string, reqBody, resType interface{}) error { return c.CallAPI("POST", url, reqBody, resType, true) } // PostUnAuth is a wrapper for the unauthenticated POST method func (c *Client) PostUnAuth(url string, reqBody, resType interface{}) error { return c.CallAPI("POST", url, reqBody, resType, false) } // Put is a wrapper for the PUT method func (c *Client) Put(url string, reqBody, resType interface{}) error { return c.CallAPI("PUT", url, reqBody, resType, true) } // PutUnAuth is a wrapper for the unauthenticated PUT method func (c *Client) PutUnAuth(url string, reqBody, resType interface{}) error { return c.CallAPI("PUT", url, reqBody, resType, false) } // Delete is a wrapper for the DELETE method func (c *Client) Delete(url string, resType interface{}) error { return c.CallAPI("DELETE", url, nil, resType, true) } // DeleteUnAuth is a wrapper for the unauthenticated DELETE method func (c *Client) DeleteUnAuth(url string, resType interface{}) error { return c.CallAPI("DELETE", url, nil, resType, false) } // GetWithContext is a wrapper for the GET method func (c *Client) GetWithContext(ctx context.Context, url string, resType interface{}) error { return c.CallAPIWithContext(ctx, "GET", url, nil, resType, true) } // GetUnAuthWithContext is a wrapper for the unauthenticated GET method func (c *Client) GetUnAuthWithContext(ctx context.Context, url string, resType interface{}) error { return c.CallAPIWithContext(ctx, "GET", url, nil, resType, false) } // PostWithContext is a wrapper for the POST method func (c *Client) PostWithContext(ctx context.Context, url string, reqBody, resType interface{}) error { return c.CallAPIWithContext(ctx, "POST", url, reqBody, resType, true) } // PostUnAuthWithContext is a wrapper for the unauthenticated POST method func (c *Client) PostUnAuthWithContext(ctx context.Context, url string, reqBody, resType interface{}) error { return c.CallAPIWithContext(ctx, "POST", url, reqBody, resType, false) } // PutWithContext is a wrapper for the PUT method func (c *Client) PutWithContext(ctx context.Context, url string, reqBody, resType interface{}) error { return c.CallAPIWithContext(ctx, "PUT", url, reqBody, resType, true) } // PutUnAuthWithContext is a wrapper for the unauthenticated PUT method func (c *Client) PutUnAuthWithContext(ctx context.Context, url string, reqBody, resType interface{}) error { return c.CallAPIWithContext(ctx, "PUT", url, reqBody, resType, false) } // DeleteWithContext is a wrapper for the DELETE method func (c *Client) DeleteWithContext(ctx context.Context, url string, resType interface{}) error { return c.CallAPIWithContext(ctx, "DELETE", url, nil, resType, true) } // DeleteUnAuthWithContext is a wrapper for the unauthenticated DELETE method func (c *Client) DeleteUnAuthWithContext(ctx context.Context, url string, resType interface{}) error { return c.CallAPIWithContext(ctx, "DELETE", url, nil, resType, false) } // timeDelta returns the time delta between the host and the remote API func (c *Client) getTimeDelta() (time.Duration, error) { if !c.timeDeltaDone { // Ensure only one thread is updating c.timeDeltaMutex.Lock() // Ensure that the mutex will be released on return defer c.timeDeltaMutex.Unlock() // Did we wait ? Maybe no more needed if !c.timeDeltaDone { ovhTime, err := c.getTime() if err != nil { return 0, err } c.timeDelta = time.Since(*ovhTime) c.timeDeltaDone = true } } return c.timeDelta, nil } // getTime t returns time from for a given api client endpoint func (c *Client) getTime() (*time.Time, error) { var timestamp int64 err := c.GetUnAuth("/auth/time", ×tamp) if err != nil { return nil, err } serverTime := time.Unix(timestamp, 0) return &serverTime, nil } // getLocalTime is a function to be overwritten during the tests, it return the time // on the the local machine var getLocalTime = func() time.Time { return time.Now() } // getEndpointForSignature is a function to be overwritten during the tests, it returns a // the endpoint var getEndpointForSignature = func(c *Client) string { return c.endpoint } // NewRequest returns a new HTTP request func (c *Client) NewRequest(method, path string, reqBody interface{}, needAuth bool) (*http.Request, error) { var body []byte var err error if reqBody != nil { body, err = json.Marshal(reqBody) if err != nil { return nil, err } } target := fmt.Sprintf("%s%s", c.endpoint, path) req, err := http.NewRequest(method, target, bytes.NewReader(body)) if err != nil { return nil, err } // Inject headers if body != nil { req.Header.Add("Content-Type", "application/json;charset=utf-8") } req.Header.Add("X-Ovh-Application", c.AppKey) req.Header.Add("Accept", "application/json") // Inject signature. Some methods do not need authentication, especially /time, // /auth and some /order methods are actually broken if authenticated. if needAuth { timeDelta, err := c.TimeDelta() if err != nil { return nil, err } timestamp := getLocalTime().Add(-timeDelta).Unix() req.Header.Add("X-Ovh-Timestamp", strconv.FormatInt(timestamp, 10)) req.Header.Add("X-Ovh-Consumer", c.ConsumerKey) h := sha1.New() h.Write([]byte(fmt.Sprintf("%s+%s+%s+%s%s+%s+%d", c.AppSecret, c.ConsumerKey, method, getEndpointForSignature(c), path, body, timestamp, ))) req.Header.Add("X-Ovh-Signature", fmt.Sprintf("$1$%x", h.Sum(nil))) } // Send the request with requested timeout c.Client.Timeout = c.Timeout return req, nil } // Do sends an HTTP request and returns an HTTP response func (c *Client) Do(req *http.Request) (*http.Response, error) { if c.Logger != nil { c.Logger.LogRequest(req) } resp, err := c.Client.Do(req) if err != nil { return nil, err } if c.Logger != nil { c.Logger.LogResponse(resp) } return resp, nil } // CallAPI is the lowest level call helper. If needAuth is true, // inject authentication headers and sign the request. // // Request signature is a sha1 hash on following fields, joined by '+': // - applicationSecret (from Client instance) // - consumerKey (from Client instance) // - capitalized method (from arguments) // - full request url, including any query string argument // - full serialized request body // - server current time (takes time delta into account) // // Call will automatically assemble the target url from the endpoint // configured in the client instance and the path argument. If the reqBody // argument is not nil, it will also serialize it as json and inject // the required Content-Type header. // // If everything went fine, unmarshall response into resType and return nil // otherwise, return the error func (c *Client) CallAPI(method, path string, reqBody, resType interface{}, needAuth bool) error { return c.CallAPIWithContext(context.Background(), method, path, reqBody, resType, needAuth) } // CallAPIWithContext is the lowest level call helper. If needAuth is true, // inject authentication headers and sign the request. // // Request signature is a sha1 hash on following fields, joined by '+': // - applicationSecret (from Client instance) // - consumerKey (from Client instance) // - capitalized method (from arguments) // - full request url, including any query string argument // - full serialized request body // - server current time (takes time delta into account) // // Context is used by http.Client to handle context cancelation // // Call will automatically assemble the target url from the endpoint // configured in the client instance and the path argument. If the reqBody // argument is not nil, it will also serialize it as json and inject // the required Content-Type header. // // If everything went fine, unmarshall response into resType and return nil // otherwise, return the error func (c *Client) CallAPIWithContext(ctx context.Context, method, path string, reqBody, resType interface{}, needAuth bool) error { req, err := c.NewRequest(method, path, reqBody, needAuth) if err != nil { return err } req = req.WithContext(ctx) response, err := c.Do(req) if err != nil { return err } return c.UnmarshalResponse(response, resType) } // UnmarshalResponse checks the response and unmarshals it into the response // type if needed Helper function, called from CallAPI func (c *Client) UnmarshalResponse(response *http.Response, resType interface{}) error { // Read all the response body defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) if err != nil { return err } // < 200 && >= 300 : API error if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { apiError := &APIError{Code: response.StatusCode} if err = json.Unmarshal(body, apiError); err != nil { apiError.Message = string(body) } apiError.QueryID = response.Header.Get("X-Ovh-QueryID") return apiError } // Nothing to unmarshal if len(body) == 0 || resType == nil { return nil } d := json.NewDecoder(bytes.NewReader(body)) d.UseNumber() return d.Decode(&resType) } go-ovh-1.1.0/ovh/ovh_test.go000066400000000000000000000423131370063425400156750ustar00rootroot00000000000000package ovh import ( "context" "fmt" "io/ioutil" "math" "net/http" "net/http/httptest" "os" "reflect" "strconv" "strings" "testing" "time" "unicode" "unicode/utf8" ) const ( // In case you wonder, these are real *revoked* credentials MockApplicationKey = "TDPKJdwZwAQPwKX2" MockApplicationSecret = "9ufkBmLaTQ9nz5yMUlg79taH0GNnzDjk" MockConsumerKey = "5mBuy6SUQcRw2ZUxg0cG68BoDKpED4KY" MockTime = 1457018875 ) type SomeData struct { IntValue int `json:"i_val,omitempty"` StringValue string `json:"s_val,omitempty"` } // // Utils // func initMockServer(InputRequest **http.Request, status int, responseBody string, requestBody *string, handlerSleep time.Duration) (*httptest.Server, *Client) { // Mock time getLocalTime = func() time.Time { return time.Unix(MockTime, 0) } // Mock hostname, in signature only getEndpointForSignature = func(c *Client) string { return "http://localhost" } // Create a fake API server ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Save input parameters *InputRequest = r defer r.Body.Close() if requestBody != nil { reqBody, err := ioutil.ReadAll(r.Body) if err == nil { *requestBody = string(reqBody[:]) } } if handlerSleep != 0 { time.Sleep(handlerSleep) } // Respond w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) fmt.Fprint(w, responseBody) })) // Create client client, _ := NewClient(ts.URL, MockApplicationKey, MockApplicationSecret, MockConsumerKey) client.timeDeltaDone = true return ts, client } func ensureHeaderPresent(t *testing.T, r *http.Request, name, value string) { val, present := r.Header[name] if !present { t.Fatalf("%s requests should include a %s header with %s value.", r.Method, name, value) } if val[0] != value { t.Fatalf("%s requests should include a %s header with %s value. Got %s", r.Method, name, value, val[0]) } } func Capitalize(s string) string { if s == "" { return "" } r, n := utf8.DecodeRuneInString(s) return string(unicode.ToUpper(r)) + strings.ToLower(s[n:]) } // // Tests // func TestTime(t *testing.T) { // Init test var InputRequest *http.Request ts, client := initMockServer(&InputRequest, 200, fmt.Sprintf("%d", MockTime), nil, time.Duration(0)) defer ts.Close() // Test serverTime, err := client.Time() if err != nil { t.Fatalf("Unexpected error while retrieving server time: %v\n", err) } // Validate if InputRequest.Method != "GET" || InputRequest.URL.String() != "/auth/time" { t.Fatalf("Time should be retrieved using GET /auth/time. Got %s %s", InputRequest.Method, InputRequest.URL.String()) } if serverTime.Unix() != MockTime { t.Fatalf("Server time should be %d. Got %d", MockTime, serverTime.Unix()) } } func TestPing(t *testing.T) { // Init test var InputRequest *http.Request ts, client := initMockServer(&InputRequest, 200, `0`, nil, time.Duration(0)) defer ts.Close() // Test err := client.Ping() // Validate if err != nil { t.Fatalf("Unexpected error while pinging server: %v\n", err) } } func TestError500HTML(t *testing.T) { // Init test var InputRequest *http.Request errHTML := `

test

` ts, client := initMockServer(&InputRequest, http.StatusServiceUnavailable, errHTML, nil, time.Duration(0)) defer ts.Close() // Test var res struct{} err := client.CallAPI("GET", "/test", nil, &res, false) // Validate if err == nil { t.Fatal("Expected error") } apiError := &APIError{ Code: http.StatusServiceUnavailable, Message: errHTML, } if err.Error() != apiError.Error() { t.Fatalf("Missmatch errors : \n%s\n%s", err, apiError) } } func TestPingUnreachable(t *testing.T) { // Init test var InputRequest *http.Request ts, client := initMockServer(&InputRequest, 200, `0`, nil, time.Duration(0)) defer ts.Close() // Test client.endpoint = "https://localhost:1/does not exist" err := client.Ping() // Validate if err == nil { t.Fatalf("Unexpected success while pinging server\n") } } // APIMethodTester applies the same sanity checks to all main Client method. It checks the // request method, path, body and headers for both authenticated and unauthenticated variants func APIMethodTester(t *testing.T, HTTPmethod string, body interface{}, expectedBody string, expectedSignature string, cancel bool, contextTimeout bool) { // Init test var InputRequest *http.Request var InputRequestBody string sleep := time.Duration(0) var failureExpected bool if cancel || contextTimeout { sleep = time.Duration(2) * time.Second failureExpected = true } ts, client := initMockServer(&InputRequest, 200, `"success"`, &InputRequestBody, sleep) defer ts.Close() // Prepare method name needAuth := expectedSignature != "" HTTPmethod = strings.ToUpper(HTTPmethod) methodName := Capitalize(HTTPmethod) if !needAuth { methodName += "UnAuth" } if failureExpected { methodName += "WithContext" } // Prepare method arguments var res interface{} var arguments []reflect.Value var ctx context.Context var cancelFunc context.CancelFunc cancelFunc = func() { t.Log("context cancelFunc called") } if cancel { ctx, cancelFunc = context.WithCancel(context.Background()) } else if contextTimeout { ctx, cancelFunc = context.WithTimeout(context.Background(), time.Duration(200)*time.Millisecond) } defer cancelFunc() if failureExpected { arguments = append(arguments, reflect.ValueOf(ctx)) } arguments = append(arguments, reflect.ValueOf("/some/resource")) if body != nil { arguments = append(arguments, reflect.ValueOf(body)) } arguments = append(arguments, reflect.ValueOf(&res)) // Get method to test method := reflect.ValueOf(client).MethodByName(methodName) if !method.IsValid() { t.Fatalf("Client should suport %s method\n", methodName) } if cancel { go func() { time.Sleep(100 * time.Millisecond) cancelFunc() }() } ret := method.Call(arguments) if failureExpected && ret[0].IsNil() { t.Fatal("Should have a context cancelation error, got nil") } else if !failureExpected && !ret[0].IsNil() { t.Fatalf("Unexpected error while retrieving server time: %v\n", ret[0]) } // Log request details to help debugging t.Logf("Request: %s %s. Authenticated=%v", InputRequest.Method, InputRequest.URL.String(), needAuth) for key, value := range InputRequest.Header { t.Logf("\tHEADER: key=%v, value=%v\n", key, value) } // Validate Method if InputRequest.Method != HTTPmethod || InputRequest.URL.String() != "/some/resource" { t.Fatalf("%s should trigger a %s /some/resource request. Got %s %s", methodName, HTTPmethod, InputRequest.Method, InputRequest.URL.String()) } // Validate Body if body != nil && expectedBody != InputRequestBody { t.Fatalf("%s /some/resource should have '%s' body. Got '%s'", methodName, expectedBody, InputRequestBody) } // Validate Headers ensureHeaderPresent(t, InputRequest, "Accept", "application/json") ensureHeaderPresent(t, InputRequest, "X-Ovh-Application", MockApplicationKey) if body != nil { ensureHeaderPresent(t, InputRequest, "Content-Type", "application/json;charset=utf-8") } if needAuth { ensureHeaderPresent(t, InputRequest, "X-Ovh-Timestamp", strconv.Itoa(MockTime)) ensureHeaderPresent(t, InputRequest, "X-Ovh-Consumer", MockConsumerKey) ensureHeaderPresent(t, InputRequest, "X-Ovh-Signature", expectedSignature) } } func TestAllAPIMethods(t *testing.T) { body := SomeData{ IntValue: 42, StringValue: "Hello World!", } APIMethodTester(t, "GET", nil, "", "$1$8a21169b341aa23e82192e07457ca978006b1ba9", false, false) APIMethodTester(t, "GET", nil, "", "", false, false) APIMethodTester(t, "DELETE", nil, "", "$1$f4571312a04a4c75188509e75c40581ca6bb6d7a", false, false) APIMethodTester(t, "DELETE", nil, "", "", false, false) APIMethodTester(t, "POST", body, `{"i_val":42,"s_val":"Hello World!"}`, "$1$6549d84e65be72f4ec0d7b6d7eaa19554a265990", false, false) APIMethodTester(t, "POST", body, `{"i_val":42,"s_val":"Hello World!"}`, "", false, false) APIMethodTester(t, "PUT", body, `{"i_val":42,"s_val":"Hello World!"}`, "$1$983e2a9a213c99211edd0b32715ac1ace1a6a0ea", false, false) APIMethodTester(t, "PUT", body, `{"i_val":42,"s_val":"Hello World!"}`, "", false, false) APIMethodTester(t, "GET", nil, "", "$1$8a21169b341aa23e82192e07457ca978006b1ba9", true, false) APIMethodTester(t, "GET", nil, "", "", true, false) APIMethodTester(t, "DELETE", nil, "", "$1$f4571312a04a4c75188509e75c40581ca6bb6d7a", true, false) APIMethodTester(t, "DELETE", nil, "", "", true, false) APIMethodTester(t, "POST", body, `{"i_val":42,"s_val":"Hello World!"}`, "$1$6549d84e65be72f4ec0d7b6d7eaa19554a265990", true, false) APIMethodTester(t, "POST", body, `{"i_val":42,"s_val":"Hello World!"}`, "", true, false) APIMethodTester(t, "PUT", body, `{"i_val":42,"s_val":"Hello World!"}`, "$1$983e2a9a213c99211edd0b32715ac1ace1a6a0ea", true, false) APIMethodTester(t, "PUT", body, `{"i_val":42,"s_val":"Hello World!"}`, "", true, false) APIMethodTester(t, "GET", nil, "", "$1$8a21169b341aa23e82192e07457ca978006b1ba9", false, true) APIMethodTester(t, "GET", nil, "", "", false, true) APIMethodTester(t, "DELETE", nil, "", "$1$f4571312a04a4c75188509e75c40581ca6bb6d7a", false, true) APIMethodTester(t, "DELETE", nil, "", "", false, true) APIMethodTester(t, "POST", body, `{"i_val":42,"s_val":"Hello World!"}`, "$1$6549d84e65be72f4ec0d7b6d7eaa19554a265990", false, true) APIMethodTester(t, "POST", body, `{"i_val":42,"s_val":"Hello World!"}`, "", false, true) APIMethodTester(t, "PUT", body, `{"i_val":42,"s_val":"Hello World!"}`, "$1$983e2a9a213c99211edd0b32715ac1ace1a6a0ea", false, true) APIMethodTester(t, "PUT", body, `{"i_val":42,"s_val":"Hello World!"}`, "", false, true) } // Mock ReadCloser, always failing type ErrorCloseReader struct{} func (ErrorCloseReader) Read(p []byte) (int, error) { return 0, fmt.Errorf("ErrorReader") } func (ErrorCloseReader) Close() error { return nil } func TestGetResponse(t *testing.T) { var err error var apiInt int mockClient := Client{} // Nominal err = mockClient.UnmarshalResponse(&http.Response{ StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader(`42`)), }, &apiInt) if err != nil { t.Fatalf("Client.UnmarshalResponse should be able to decode int when status is 200. Got %v", err) } // Nominal: empty body err = mockClient.UnmarshalResponse(&http.Response{ StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader(``)), }, nil) if err != nil { t.Fatalf("UnmarshalResponse should not return an error when reponse is empty or target type is nil. Got %v", err) } // Error err = mockClient.UnmarshalResponse(&http.Response{ StatusCode: 400, Body: ioutil.NopCloser(strings.NewReader(`{"code": 400, "message": "Ooops..."}`)), }, &apiInt) if err == nil { t.Fatalf("Client.UnmarshalResponse should be able to decode an error when status is 400") } if _, ok := err.(*APIError); !ok { t.Fatalf("Client.UnmarshalResponse error should be an APIError when status is 400. Got '%s' of type %s", err, reflect.TypeOf(err)) } // Error: body read error err = mockClient.UnmarshalResponse(&http.Response{ Body: ErrorCloseReader{}, }, nil) if err == nil { t.Fatalf("UnmarshalResponse should return an error when failing to read HTTP Response body. %v", err) } // Error: HTTP Error + broken json err = mockClient.UnmarshalResponse(&http.Response{ StatusCode: 400, Body: ioutil.NopCloser(strings.NewReader(`{"code": 400, "mes`)), }, nil) if err == nil { t.Fatalf("UnmarshalResponse should return an error when failing to decode HTTP Response body. %v", err) } // Error with QueryID responseHeaders := http.Header{} responseHeaders.Add("X-Ovh-QueryID", "FR.ws-8.5860f657.4632.0180") err = mockClient.UnmarshalResponse(&http.Response{ StatusCode: 400, Body: ioutil.NopCloser(strings.NewReader(`{"code": 400, "message": "Ooops..."}`)), Header: responseHeaders, }, &apiInt) apiErr, ok := err.(*APIError) if !ok { t.Fatalf("Client.UnmarshalResponse error should be an APIError when status is 400 and header QueryID is found. Got '%s' of type %s", err, reflect.TypeOf(err)) } if apiErr.QueryID != "FR.ws-8.5860f657.4632.0180" { t.Fatalf("APIError should be filled with a correct QueryID. Got '%s' instead of '%s'", apiErr.QueryID, "FR.ws-8.5860f657.4632.0180") } } func TestGetResponseUnmarshalNumber(t *testing.T) { var err error var output map[string]interface{} mockClient := Client{} // with map[string]interface{} as output err = mockClient.UnmarshalResponse(&http.Response{ StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader(`{"orderId": 1234567890}`)), }, &output) if err != nil { t.Fatalf("Client.UnmarshalResponse should be able to decode the body") } if "1234567890" != fmt.Sprint(output["orderId"]) { t.Fatalf("Client.UnmarshalResponse should unmarshal long integer as json.Number instead of float64, stringified incorrectly") } var outputInt map[string]int64 // with map[string]int64 as output err = mockClient.UnmarshalResponse(&http.Response{ StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader(`{"orderId": 1234567890}`)), }, &outputInt) if err != nil { t.Fatalf("Client.UnmarshalResponse should be able to decode the body") } if int64(1234567890) != outputInt["orderId"] { t.Fatalf("Client.UnmarshalResponse should unmarshal long integer as json.Number instead of float64, incorrectly casted as int64") } var outputFloat map[string]float64 // with map[string]int64 as output err = mockClient.UnmarshalResponse(&http.Response{ StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader(`{"orderId": 1234567890}`)), }, &outputFloat) if err != nil { t.Fatalf("Client.UnmarshalResponse should be able to decode the body") } if float64(1234567890) != outputFloat["orderId"] { t.Fatalf("Client.UnmarshalResponse should unmarshal long integer as json.Number instead of float64, incorrectly casted as float64") } } func TestConstructors(t *testing.T) { // Nominal: full constructor client, err := NewClient("ovh-eu", MockApplicationKey, MockApplicationSecret, MockConsumerKey) if err != nil { t.Fatalf("NewClient should not return an error in the nominal case. Got: %v", err) } if client.Client == nil { t.Fatalf("client.Client should be a valid HTTP client") } if client.AppKey != MockApplicationKey { t.Fatalf("client.AppKey should be '%s'. Got '%s'", MockApplicationKey, client.AppKey) } if client.AppSecret != MockApplicationSecret { t.Fatalf("client.AppSecret should be '%s'. Got '%s'", MockApplicationSecret, client.AppSecret) } if client.ConsumerKey != MockConsumerKey { t.Fatalf("client.ConsumerKey should be '%s'. Got '%s'", MockConsumerKey, client.ConsumerKey) } // Nominal: Endpoint constructor os.Setenv("OVH_APPLICATION_KEY", MockApplicationKey) os.Setenv("OVH_APPLICATION_SECRET", MockApplicationSecret) os.Setenv("OVH_CONSUMER_KEY", MockConsumerKey) client, err = NewEndpointClient("ovh-eu") if err != nil { t.Fatalf("NewEndpointClient should not return an error in the nominal case. Got: %v", err) } if client.Client == nil { t.Fatalf("client.Client should be a valid HTTP client") } if client.AppKey != MockApplicationKey { t.Fatalf("client.AppKey should be '%s'. Got '%s'", MockApplicationKey, client.AppKey) } if client.AppSecret != MockApplicationSecret { t.Fatalf("client.AppSecret should be '%s'. Got '%s'", MockApplicationSecret, client.AppSecret) } if client.ConsumerKey != MockConsumerKey { t.Fatalf("client.ConsumerKey should be '%s'. Got '%s'", MockConsumerKey, client.ConsumerKey) } // Nominal: Default constructor os.Setenv("OVH_ENDPOINT", "ovh-eu") client, err = NewDefaultClient() if err != nil { t.Fatalf("NewEndpointClient should not return an error in the nominal case. Got: %v", err) } if client.Client == nil { t.Fatalf("client.Client should be a valid HTTP client") } if client.endpoint != "https://eu.api.ovh.com/1.0" { t.Fatalf("client.Endpoint should be 'https://eu.api.ovh.com/1.0'. Got '%s'", client.endpoint) } // Clear os.Unsetenv("OVH_ENDPOINT") os.Unsetenv("OVH_APPLICATION_KEY") os.Unsetenv("OVH_APPLICATION_SECRET") os.Unsetenv("OVH_CONSUMER_KEY") // Error: missing Endpoint _, err = NewClient("", MockApplicationKey, MockApplicationSecret, MockConsumerKey) if err == nil { t.Fatalf("NewClient should return an error when missing Endpoint") } // Error: missing ApplicationKey _, err = NewClient("ovh-eu", "", MockApplicationSecret, MockConsumerKey) if err == nil { t.Fatalf("NewClient should return an error when missing ApplicationKey") } // Error: missing ApplicationSecret _, err = NewClient("ovh-eu", MockConsumerKey, "", MockConsumerKey) if err == nil { t.Fatalf("NewClient should return an error when missing ApplicationSecret") } } func TestGetTimeDelta(t *testing.T) { MockDelta := 747 // Init test var InputRequest *http.Request ts, client := initMockServer(&InputRequest, 200, fmt.Sprintf("%d", int(time.Now().Unix())-MockDelta), nil, time.Duration(0)) defer ts.Close() // Test client.timeDeltaDone = false delta, err := client.getTimeDelta() if err != nil { t.Fatalf("getTimeDelta should not return an error. Got %v", err) } // Hack: take races into account, avoid mocking whole earth if math.Abs(float64(delta/time.Second-time.Duration(MockDelta))) > 2 { t.Fatalf("getTimeDelta should return a delta of %d. Got %d", time.Duration(MockDelta)*time.Second, delta) } }