diff --git a/cmd/keel/main.go b/cmd/keel/main.go index d653d66d..31d3f717 100644 --- a/cmd/keel/main.go +++ b/cmd/keel/main.go @@ -30,6 +30,7 @@ import ( "github.com/keel-hq/keel/version" // extensions + _ "github.com/keel-hq/keel/extension/notification/hipchat" _ "github.com/keel-hq/keel/extension/notification/slack" _ "github.com/keel-hq/keel/extension/notification/webhook" diff --git a/constants/constants.go b/constants/constants.go index e263cdd2..e64caffa 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -11,10 +11,14 @@ const WebhookEndpointEnv = "WEBHOOK_ENDPOINT" // slack bot/token const ( - EnvSlackToken = "SLACK_TOKEN" - EnvSlackBotName = "SLACK_BOT_NAME" - EnvSlackChannels = "SLACK_CHANNELS" + EnvSlackToken = "SLACK_TOKEN" + EnvSlackBotName = "SLACK_BOT_NAME" + EnvSlackChannels = "SLACK_CHANNELS" EnvSlackApprovalsChannel = "SLACK_APPROVALS_CHANNEL" + + EnvHipchatToken = "HIPCHAT_TOKEN" + EnvHipchatBotName = "HIPCHAT_BOT_NAME" + EnvHipchatChannels = "HIPCHAT_CHANNELS" ) // EnvNotificationLevel - minimum level for notifications, defaults to info diff --git a/extension/notification/hipchat/hipchat.go b/extension/notification/hipchat/hipchat.go new file mode 100644 index 00000000..af117d2d --- /dev/null +++ b/extension/notification/hipchat/hipchat.go @@ -0,0 +1,96 @@ +package hipchat + +import ( + "fmt" + "os" + "strings" + + "github.com/tbruyelle/hipchat-go/hipchat" + + "github.com/keel-hq/keel/constants" + "github.com/keel-hq/keel/extension/notification" + "github.com/keel-hq/keel/types" + + log "github.com/Sirupsen/logrus" +) + +type sender struct { + hipchatClient *hipchat.Client + channels []string + botName string +} + +func init() { + notification.RegisterSender("hipchat", &sender{}) +} + +func (s *sender) Configure(config *notification.Config) (bool, error) { + var token string + + if os.Getenv(constants.EnvHipchatToken) != "" { + token = os.Getenv(constants.EnvHipchatToken) + } else { + return false, nil + } + if os.Getenv(constants.EnvHipchatBotName) != "" { + s.botName = os.Getenv(constants.EnvHipchatBotName) + } else { + s.botName = "keel" + } + + if os.Getenv(constants.EnvHipchatChannels) != "" { + channels := os.Getenv(constants.EnvHipchatChannels) + s.channels = strings.Split(channels, ",") + } else { + s.channels = []string{"general"} + } + + s.hipchatClient = hipchat.NewClient(token) + + log.WithFields(log.Fields{ + "name": "hipchat", + "channels": s.channels, + }).Info("extension.notification.hipchat: sender configured") + + return true, nil +} + +func (s *sender) Send(event types.EventNotification) error { + msg := fmt.Sprintf("%s
%s", event.Type.String(), event.Message) + + notification := &hipchat.NotificationRequest{ + Color: getHipchatColor(event.Level.String()), + Message: msg, + Notify: true, + From: s.botName, + } + + for _, channel := range s.channels { + _, err := s.hipchatClient.Room.Notification(channel, notification) + if err != nil { + log.WithFields(log.Fields{ + "error": err, + "channel": channel, + }).Error("extension.notification.hipchat: failed to send notification") + } + } + + return nil +} + +func getHipchatColor(eventLevel string) hipchat.Color { + switch eventLevel { + case "error": + return "red" + case "info": + return "gray" + case "success": + return "green" + case "fatal": + return "purple" + case "warn": + return "yellow" + default: + return "gray" + } +} diff --git a/hack/deployment.sample.yml b/hack/deployment.sample.yml index d893025f..569ec13d 100644 --- a/hack/deployment.sample.yml +++ b/hack/deployment.sample.yml @@ -25,7 +25,7 @@ spec: value: "1" # Enable/disable Helm provider # - name: HELM_PROVIDER - # value: "1" + # value: "1" - name: PROJECT_ID value: "my-project-id" # - name: WEBHOOK_ENDPOINT @@ -36,6 +36,10 @@ spec: # value: general # - name: SLACK_APPROVALS_CHANNEL # value: approvals + # - name: HIPCHAT_TOKEN + # value: your-token-here + # - name: HIPCHAT_CHANNELS + # value: keel-bot name: keel command: ["/bin/keel"] ports: diff --git a/vendor/github.com/google/go-querystring/.gitignore b/vendor/github.com/google/go-querystring/.gitignore new file mode 100644 index 00000000..9ed3b07c --- /dev/null +++ b/vendor/github.com/google/go-querystring/.gitignore @@ -0,0 +1 @@ +*.test diff --git a/vendor/github.com/google/go-querystring/CONTRIBUTING.md b/vendor/github.com/google/go-querystring/CONTRIBUTING.md new file mode 100644 index 00000000..51cf5cd1 --- /dev/null +++ b/vendor/github.com/google/go-querystring/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# How to contribute # + +We'd love to accept your patches and contributions to this project. There are +a just a few small guidelines you need to follow. + + +## Contributor License Agreement ## + +Contributions to any Google project must be accompanied by a Contributor +License Agreement. This is not a copyright **assignment**, it simply gives +Google permission to use and redistribute your contributions as part of the +project. + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual + CLA][]. + + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA][]. + +You generally only need to submit a CLA once, so if you've already submitted +one (even if it was for a different project), you probably don't need to do it +again. + +[individual CLA]: https://developers.google.com/open-source/cla/individual +[corporate CLA]: https://developers.google.com/open-source/cla/corporate + + +## Submitting a patch ## + + 1. It's generally best to start by opening a new issue describing the bug or + feature you're intending to fix. Even if you think it's relatively minor, + it's helpful to know what people are working on. Mention in the initial + issue that you are planning to work on that bug or feature so that it can + be assigned to you. + + 1. Follow the normal process of [forking][] the project, and setup a new + branch to work in. It's important that each group of changes be done in + separate branches in order to ensure that a pull request only includes the + commits related to that bug or feature. + + 1. Go makes it very simple to ensure properly formatted code, so always run + `go fmt` on your code before committing it. You should also run + [golint][] over your code. As noted in the [golint readme][], it's not + strictly necessary that your code be completely "lint-free", but this will + help you find common style issues. + + 1. Any significant changes should almost always be accompanied by tests. The + project already has good test coverage, so look at some of the existing + tests if you're unsure how to go about it. [gocov][] and [gocov-html][] + are invaluable tools for seeing which parts of your code aren't being + exercised by your tests. + + 1. Do your best to have [well-formed commit messages][] for each change. + This provides consistency throughout the project, and ensures that commit + messages are able to be formatted properly by various git tools. + + 1. Finally, push the commits to your fork and submit a [pull request][]. + +[forking]: https://help.github.com/articles/fork-a-repo +[golint]: https://github.com/golang/lint +[golint readme]: https://github.com/golang/lint/blob/master/README +[gocov]: https://github.com/axw/gocov +[gocov-html]: https://github.com/matm/gocov-html +[well-formed commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html +[squash]: http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits +[pull request]: https://help.github.com/articles/creating-a-pull-request diff --git a/vendor/github.com/google/go-querystring/LICENSE b/vendor/github.com/google/go-querystring/LICENSE new file mode 100644 index 00000000..ae121a1e --- /dev/null +++ b/vendor/github.com/google/go-querystring/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013 Google. 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 Google Inc. 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 THE COPYRIGHT HOLDERS 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 THE COPYRIGHT +OWNER OR 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. diff --git a/vendor/github.com/google/go-querystring/README.md b/vendor/github.com/google/go-querystring/README.md new file mode 100644 index 00000000..03e93703 --- /dev/null +++ b/vendor/github.com/google/go-querystring/README.md @@ -0,0 +1,39 @@ +# go-querystring # + +go-querystring is Go library for encoding structs into URL query parameters. + + +**Documentation:** +**Build Status:** [![Build Status](https://drone.io/github.com/google/go-querystring/status.png)](https://drone.io/github.com/google/go-querystring/latest) + +## Usage ## + +```go +import "github.com/google/go-querystring/query" +``` + +go-querystring is designed to assist in scenarios where you want to construct a +URL using a struct that represents the URL query parameters. You might do this +to enforce the type safety of your parameters, for example, as is done in the +[go-github][] library. + +The query package exports a single `Values()` function. A simple example: + +```go +type Options struct { + Query string `url:"q"` + ShowAll bool `url:"all"` + Page int `url:"page"` +} + +opt := Options{ "foo", true, 2 } +v, _ := query.Values(opt) +fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2" +``` + +[go-github]: https://github.com/google/go-github/commit/994f6f8405f052a117d2d0b500054341048fbb08 + +## License ## + +This library is distributed under the BSD-style license found in the [LICENSE](./LICENSE) +file. diff --git a/vendor/github.com/google/go-querystring/query/encode.go b/vendor/github.com/google/go-querystring/query/encode.go new file mode 100644 index 00000000..37080b19 --- /dev/null +++ b/vendor/github.com/google/go-querystring/query/encode.go @@ -0,0 +1,320 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package query implements encoding of structs into URL query parameters. +// +// As a simple example: +// +// type Options struct { +// Query string `url:"q"` +// ShowAll bool `url:"all"` +// Page int `url:"page"` +// } +// +// opt := Options{ "foo", true, 2 } +// v, _ := query.Values(opt) +// fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2" +// +// The exact mapping between Go values and url.Values is described in the +// documentation for the Values() function. +package query + +import ( + "bytes" + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + "time" +) + +var timeType = reflect.TypeOf(time.Time{}) + +var encoderType = reflect.TypeOf(new(Encoder)).Elem() + +// Encoder is an interface implemented by any type that wishes to encode +// itself into URL values in a non-standard way. +type Encoder interface { + EncodeValues(key string, v *url.Values) error +} + +// Values returns the url.Values encoding of v. +// +// Values expects to be passed a struct, and traverses it recursively using the +// following encoding rules. +// +// Each exported struct field is encoded as a URL parameter unless +// +// - the field's tag is "-", or +// - the field is empty and its tag specifies the "omitempty" option +// +// The empty values are false, 0, any nil pointer or interface value, any array +// slice, map, or string of length zero, and any time.Time that returns true +// for IsZero(). +// +// The URL parameter name defaults to the struct field name but can be +// specified in the struct field's tag value. The "url" key in the struct +// field's tag value is the key name, followed by an optional comma and +// options. For example: +// +// // Field is ignored by this package. +// Field int `url:"-"` +// +// // Field appears as URL parameter "myName". +// Field int `url:"myName"` +// +// // Field appears as URL parameter "myName" and the field is omitted if +// // its value is empty +// Field int `url:"myName,omitempty"` +// +// // Field appears as URL parameter "Field" (the default), but the field +// // is skipped if empty. Note the leading comma. +// Field int `url:",omitempty"` +// +// For encoding individual field values, the following type-dependent rules +// apply: +// +// Boolean values default to encoding as the strings "true" or "false". +// Including the "int" option signals that the field should be encoded as the +// strings "1" or "0". +// +// time.Time values default to encoding as RFC3339 timestamps. Including the +// "unix" option signals that the field should be encoded as a Unix time (see +// time.Unix()) +// +// Slice and Array values default to encoding as multiple URL values of the +// same name. Including the "comma" option signals that the field should be +// encoded as a single comma-delimited value. Including the "space" option +// similarly encodes the value as a single space-delimited string. Including +// the "semicolon" option will encode the value as a semicolon-delimited string. +// Including the "brackets" option signals that the multiple URL values should +// have "[]" appended to the value name. "numbered" will append a number to +// the end of each incidence of the value name, example: +// name0=value0&name1=value1, etc. +// +// Anonymous struct fields are usually encoded as if their inner exported +// fields were fields in the outer struct, subject to the standard Go +// visibility rules. An anonymous struct field with a name given in its URL +// tag is treated as having that name, rather than being anonymous. +// +// Non-nil pointer values are encoded as the value pointed to. +// +// Nested structs are encoded including parent fields in value names for +// scoping. e.g: +// +// "user[name]=acme&user[addr][postcode]=1234&user[addr][city]=SFO" +// +// All other values are encoded using their default string representation. +// +// Multiple fields that encode to the same URL parameter name will be included +// as multiple URL values of the same name. +func Values(v interface{}) (url.Values, error) { + values := make(url.Values) + val := reflect.ValueOf(v) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return values, nil + } + val = val.Elem() + } + + if v == nil { + return values, nil + } + + if val.Kind() != reflect.Struct { + return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind()) + } + + err := reflectValue(values, val, "") + return values, err +} + +// reflectValue populates the values parameter from the struct fields in val. +// Embedded structs are followed recursively (using the rules defined in the +// Values function documentation) breadth-first. +func reflectValue(values url.Values, val reflect.Value, scope string) error { + var embedded []reflect.Value + + typ := val.Type() + for i := 0; i < typ.NumField(); i++ { + sf := typ.Field(i) + if sf.PkgPath != "" && !sf.Anonymous { // unexported + continue + } + + sv := val.Field(i) + tag := sf.Tag.Get("url") + if tag == "-" { + continue + } + name, opts := parseTag(tag) + if name == "" { + if sf.Anonymous && sv.Kind() == reflect.Struct { + // save embedded struct for later processing + embedded = append(embedded, sv) + continue + } + + name = sf.Name + } + + if scope != "" { + name = scope + "[" + name + "]" + } + + if opts.Contains("omitempty") && isEmptyValue(sv) { + continue + } + + if sv.Type().Implements(encoderType) { + if !reflect.Indirect(sv).IsValid() { + sv = reflect.New(sv.Type().Elem()) + } + + m := sv.Interface().(Encoder) + if err := m.EncodeValues(name, &values); err != nil { + return err + } + continue + } + + if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array { + var del byte + if opts.Contains("comma") { + del = ',' + } else if opts.Contains("space") { + del = ' ' + } else if opts.Contains("semicolon") { + del = ';' + } else if opts.Contains("brackets") { + name = name + "[]" + } + + if del != 0 { + s := new(bytes.Buffer) + first := true + for i := 0; i < sv.Len(); i++ { + if first { + first = false + } else { + s.WriteByte(del) + } + s.WriteString(valueString(sv.Index(i), opts)) + } + values.Add(name, s.String()) + } else { + for i := 0; i < sv.Len(); i++ { + k := name + if opts.Contains("numbered") { + k = fmt.Sprintf("%s%d", name, i) + } + values.Add(k, valueString(sv.Index(i), opts)) + } + } + continue + } + + for sv.Kind() == reflect.Ptr { + if sv.IsNil() { + break + } + sv = sv.Elem() + } + + if sv.Type() == timeType { + values.Add(name, valueString(sv, opts)) + continue + } + + if sv.Kind() == reflect.Struct { + reflectValue(values, sv, name) + continue + } + + values.Add(name, valueString(sv, opts)) + } + + for _, f := range embedded { + if err := reflectValue(values, f, scope); err != nil { + return err + } + } + + return nil +} + +// valueString returns the string representation of a value. +func valueString(v reflect.Value, opts tagOptions) string { + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return "" + } + v = v.Elem() + } + + if v.Kind() == reflect.Bool && opts.Contains("int") { + if v.Bool() { + return "1" + } + return "0" + } + + if v.Type() == timeType { + t := v.Interface().(time.Time) + if opts.Contains("unix") { + return strconv.FormatInt(t.Unix(), 10) + } + return t.Format(time.RFC3339) + } + + return fmt.Sprint(v.Interface()) +} + +// isEmptyValue checks if a value should be considered empty for the purposes +// of omitting fields with the "omitempty" option. +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + + if v.Type() == timeType { + return v.Interface().(time.Time).IsZero() + } + + return false +} + +// tagOptions is the string following a comma in a struct field's "url" tag, or +// the empty string. It does not include the leading comma. +type tagOptions []string + +// parseTag splits a struct field's url tag into its name and comma-separated +// options. +func parseTag(tag string) (string, tagOptions) { + s := strings.Split(tag, ",") + return s[0], s[1:] +} + +// Contains checks whether the tagOptions contains the specified option. +func (o tagOptions) Contains(option string) bool { + for _, s := range o { + if s == option { + return true + } + } + return false +} diff --git a/vendor/github.com/google/go-querystring/query/encode_test.go b/vendor/github.com/google/go-querystring/query/encode_test.go new file mode 100644 index 00000000..0f26a775 --- /dev/null +++ b/vendor/github.com/google/go-querystring/query/encode_test.go @@ -0,0 +1,328 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package query + +import ( + "fmt" + "net/url" + "reflect" + "testing" + "time" +) + +type Nested struct { + A SubNested `url:"a"` + B *SubNested `url:"b"` + Ptr *SubNested `url:"ptr,omitempty"` +} + +type SubNested struct { + Value string `url:"value"` +} + +func TestValues_types(t *testing.T) { + str := "string" + strPtr := &str + timeVal := time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC) + + tests := []struct { + in interface{} + want url.Values + }{ + { + // basic primitives + struct { + A string + B int + C uint + D float32 + E bool + }{}, + url.Values{ + "A": {""}, + "B": {"0"}, + "C": {"0"}, + "D": {"0"}, + "E": {"false"}, + }, + }, + { + // pointers + struct { + A *string + B *int + C **string + D *time.Time + }{ + A: strPtr, + C: &strPtr, + D: &timeVal, + }, + url.Values{ + "A": {str}, + "B": {""}, + "C": {str}, + "D": {"2000-01-01T12:34:56Z"}, + }, + }, + { + // slices and arrays + struct { + A []string + B []string `url:",comma"` + C []string `url:",space"` + D [2]string + E [2]string `url:",comma"` + F [2]string `url:",space"` + G []*string `url:",space"` + H []bool `url:",int,space"` + I []string `url:",brackets"` + J []string `url:",semicolon"` + K []string `url:",numbered"` + }{ + A: []string{"a", "b"}, + B: []string{"a", "b"}, + C: []string{"a", "b"}, + D: [2]string{"a", "b"}, + E: [2]string{"a", "b"}, + F: [2]string{"a", "b"}, + G: []*string{&str, &str}, + H: []bool{true, false}, + I: []string{"a", "b"}, + J: []string{"a", "b"}, + K: []string{"a", "b"}, + }, + url.Values{ + "A": {"a", "b"}, + "B": {"a,b"}, + "C": {"a b"}, + "D": {"a", "b"}, + "E": {"a,b"}, + "F": {"a b"}, + "G": {"string string"}, + "H": {"1 0"}, + "I[]": {"a", "b"}, + "J": {"a;b"}, + "K0": {"a"}, + "K1": {"b"}, + }, + }, + { + // other types + struct { + A time.Time + B time.Time `url:",unix"` + C bool `url:",int"` + D bool `url:",int"` + }{ + A: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC), + B: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC), + C: true, + D: false, + }, + url.Values{ + "A": {"2000-01-01T12:34:56Z"}, + "B": {"946730096"}, + "C": {"1"}, + "D": {"0"}, + }, + }, + { + struct { + Nest Nested `url:"nest"` + }{ + Nested{ + A: SubNested{ + Value: "that", + }, + }, + }, + url.Values{ + "nest[a][value]": {"that"}, + "nest[b]": {""}, + }, + }, + { + struct { + Nest Nested `url:"nest"` + }{ + Nested{ + Ptr: &SubNested{ + Value: "that", + }, + }, + }, + url.Values{ + "nest[a][value]": {""}, + "nest[b]": {""}, + "nest[ptr][value]": {"that"}, + }, + }, + { + nil, + url.Values{}, + }, + } + + for i, tt := range tests { + v, err := Values(tt.in) + if err != nil { + t.Errorf("%d. Values(%q) returned error: %v", i, tt.in, err) + } + + if !reflect.DeepEqual(tt.want, v) { + t.Errorf("%d. Values(%q) returned %v, want %v", i, tt.in, v, tt.want) + } + } +} + +func TestValues_omitEmpty(t *testing.T) { + str := "" + s := struct { + a string + A string + B string `url:",omitempty"` + C string `url:"-"` + D string `url:"omitempty"` // actually named omitempty, not an option + E *string `url:",omitempty"` + }{E: &str} + + v, err := Values(s) + if err != nil { + t.Errorf("Values(%q) returned error: %v", s, err) + } + + want := url.Values{ + "A": {""}, + "omitempty": {""}, + "E": {""}, // E is included because the pointer is not empty, even though the string being pointed to is + } + if !reflect.DeepEqual(want, v) { + t.Errorf("Values(%q) returned %v, want %v", s, v, want) + } +} + +type A struct { + B +} + +type B struct { + C string +} + +type D struct { + B + C string +} + +type e struct { + B + C string +} + +type F struct { + e +} + +func TestValues_embeddedStructs(t *testing.T) { + tests := []struct { + in interface{} + want url.Values + }{ + { + A{B{C: "foo"}}, + url.Values{"C": {"foo"}}, + }, + { + D{B: B{C: "bar"}, C: "foo"}, + url.Values{"C": {"foo", "bar"}}, + }, + { + F{e{B: B{C: "bar"}, C: "foo"}}, // With unexported embed + url.Values{"C": {"foo", "bar"}}, + }, + } + + for i, tt := range tests { + v, err := Values(tt.in) + if err != nil { + t.Errorf("%d. Values(%q) returned error: %v", i, tt.in, err) + } + + if !reflect.DeepEqual(tt.want, v) { + t.Errorf("%d. Values(%q) returned %v, want %v", i, tt.in, v, tt.want) + } + } +} + +func TestValues_invalidInput(t *testing.T) { + _, err := Values("") + if err == nil { + t.Errorf("expected Values() to return an error on invalid input") + } +} + +type EncodedArgs []string + +func (m EncodedArgs) EncodeValues(key string, v *url.Values) error { + for i, arg := range m { + v.Set(fmt.Sprintf("%s.%d", key, i), arg) + } + return nil +} + +func TestValues_Marshaler(t *testing.T) { + s := struct { + Args EncodedArgs `url:"arg"` + }{[]string{"a", "b", "c"}} + v, err := Values(s) + if err != nil { + t.Errorf("Values(%q) returned error: %v", s, err) + } + + want := url.Values{ + "arg.0": {"a"}, + "arg.1": {"b"}, + "arg.2": {"c"}, + } + if !reflect.DeepEqual(want, v) { + t.Errorf("Values(%q) returned %v, want %v", s, v, want) + } +} + +func TestValues_MarshalerWithNilPointer(t *testing.T) { + s := struct { + Args *EncodedArgs `url:"arg"` + }{} + v, err := Values(s) + if err != nil { + t.Errorf("Values(%q) returned error: %v", s, err) + } + + want := url.Values{} + if !reflect.DeepEqual(want, v) { + t.Errorf("Values(%q) returned %v, want %v", s, v, want) + } +} + +func TestTagParsing(t *testing.T) { + name, opts := parseTag("field,foobar,foo") + if name != "field" { + t.Fatalf("name = %q, want field", name) + } + for _, tt := range []struct { + opt string + want bool + }{ + {"foobar", true}, + {"foo", true}, + {"bar", false}, + {"field", false}, + } { + if opts.Contains(tt.opt) != tt.want { + t.Errorf("Contains(%q) = %v", tt.opt, !tt.want) + } + } +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/.gitignore b/vendor/github.com/tbruyelle/hipchat-go/.gitignore new file mode 100644 index 00000000..97f5c520 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/.gitignore @@ -0,0 +1,2 @@ +*.swp +examples/hiptest diff --git a/vendor/github.com/tbruyelle/hipchat-go/.travis.yml b/vendor/github.com/tbruyelle/hipchat-go/.travis.yml new file mode 100644 index 00000000..e3ea49fc --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/.travis.yml @@ -0,0 +1,17 @@ +language: go +sudo: false +go: + - 1.6 + - 1.7 + - 1.8 + +install: go get -v ./hipchat +script: + - go get -u github.com/golang/lint/golint + - golint ./... + - test `gofmt -l . | wc -l` = 0 + - make + +matrix: + allow_failures: + go: tip diff --git a/vendor/github.com/tbruyelle/hipchat-go/LICENSE b/vendor/github.com/tbruyelle/hipchat-go/LICENSE new file mode 100644 index 00000000..e06d2081 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/LICENSE @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/vendor/github.com/tbruyelle/hipchat-go/Makefile b/vendor/github.com/tbruyelle/hipchat-go/Makefile new file mode 100644 index 00000000..d2a34c6a --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/Makefile @@ -0,0 +1,9 @@ +SRC_DIR=./hipchat + +include checks.mk + +default: test checks + +# test runs the unit tests and vets the code +test: + go test -v $(SRC_DIR) $(TESTARGS) -timeout=30s -parallel=4 diff --git a/vendor/github.com/tbruyelle/hipchat-go/README.md b/vendor/github.com/tbruyelle/hipchat-go/README.md new file mode 100644 index 00000000..003a5dd0 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/README.md @@ -0,0 +1,58 @@ +# hipchat-go + +Go client library for the [HipChat API v2](https://www.hipchat.com/docs/apiv2). + +[![GoDoc](https://godoc.org/github.com/tbruyelle/hipchat-go/hipchat?status.svg)](https://godoc.org/github.com/tbruyelle/hipchat-go/hipchat) +[![Build Status](https://travis-ci.org/tbruyelle/hipchat-go.svg??branch=master)](https://travis-ci.org/tbruyelle/hipchat-go) + +Currently only a small part of the API is implemented, so pull requests are welcome. + +### Usage + +```go +import "github.com/tbruyelle/hipchat-go/hipchat" +``` + +Build a new client, then use the `client.Room` service to spam all the rooms you have access to (not recommended): + +```go +c := hipchat.NewClient("") + +opt := &hipchat.RoomsListOptions{IncludePrivate: true, IncludeArchived: true} +rooms, _, err := c.Room.List(opt) +if err != nil { + panic(err) +} + +notifRq := &hipchat.NotificationRequest{Message: "Hey there!"} + +for _, room := range rooms.Items { + _, err := c.Room.Notification(room.Name, notifRq) + if err != nil { + panic(err) + } +} +``` + +### Testing the auth token + +HipChat allows to [test the auth token](https://www.hipchat.com/docs/apiv2/auth#auth_test) by adding the `auth_test=true` param, into any API endpoints. + +You can do this with `hipchat-go` by setting the global var `hipchat.AuthTest`. Because the server response will be different from the one defined in the API endpoint, you need to check another global var `AuthTestReponse` to see if the authentication succeeds. + +```go +hipchat.AuthTest = true + +client.Room.Get(42) + +_, ok := hipchat.AuthTestResponse["success"] +fmt.Println("Authentification succeed :", ok) +// Dont forget to reset the variable, or every other API calls +// will be impacted. +hipchat.AuthTest = false +``` + +--- +The code architecture is hugely inspired by [google/go-github](http://github.com/google/go-github). + + diff --git a/vendor/github.com/tbruyelle/hipchat-go/checks.mk b/vendor/github.com/tbruyelle/hipchat-go/checks.mk new file mode 100644 index 00000000..40af798b --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/checks.mk @@ -0,0 +1,18 @@ +checks: + go test -race $(SRC_DIR) + @$(call checkbin,go tool vet,golang.org/x/tools/cms/vet) + go tool vet $(SRC_DIR) + @$(call checkbin,golint,github.com/golang/lint/golint) + golint -set_exit_status $(SRC_DIR) + @$(call checkbin,errcheck,github.com/kisielk/errcheck) + errcheck -ignore 'Close' -ignoretests $(SRC_DIR) + @$(call checkbin,structcheck,github.com/opennota/check/cmd/structcheck) + structcheck $(SRC_DIR) + @$(call checkbin,varcheck,github.com/opennota/check/cmd/varcheck) + varcheck $(SRC_DIR) + +checkbin = $1 2> /dev/null; if [ $$? -eq 127 ]; then\ + echo "Retrieving missing tool $1...";\ + go get $2; \ + fi; + diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/.gitignore b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/.gitignore new file mode 100644 index 00000000..a8292681 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/.gitignore @@ -0,0 +1 @@ +hipfile diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/README.md b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/README.md new file mode 100644 index 00000000..44f3b717 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/README.md @@ -0,0 +1,21 @@ +hipfile +===== + +Sends the given file to the specified room or user. + +##### Usage + +```bash +go build +./hipfile --token= --room= --path= +``` + +##### Example + +Give it a try with the gopher.png file + +```bash +go build +./hipfile --token= --room= --path=gopher.png --message="Check out this one!" +``` + diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/gopher.png b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/gopher.png new file mode 100644 index 00000000..1eb81f0b Binary files /dev/null and b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/gopher.png differ diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/main.go b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/main.go new file mode 100644 index 00000000..dc3e75c0 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/tbruyelle/hipchat-go/hipchat" +) + +var ( + token = flag.String("token", "", "The HipChat AuthToken") + roomId = flag.String("room", "", "The HipChat room id") + userId = flag.String("user", "", "The HipChat user id") + path = flag.String("path", "", "The file path") + message = flag.String("message", "", "The message") + filename = flag.String("filename", "", "The name of the file") +) + +func main() { + flag.Parse() + if *token == "" || *path == "" || ((*roomId == "") && (*userId == "")) { + flag.PrintDefaults() + return + } + c := hipchat.NewClient(*token) + + shareFileRq := &hipchat.ShareFileRequest{Path: *path, Message: *message, Filename: *filename} + + if *roomId != "" { + resp, err := c.Room.ShareFile(*roomId, shareFileRq) + + if err != nil { + fmt.Printf("Error during room file share %q\n", err) + fmt.Printf("Server returns %+v\n", resp) + return + } + } + + if *userId != "" { + resp, err := c.User.ShareFile(*userId, shareFileRq) + + if err != nil { + fmt.Printf("Error during user file share %q\n", err) + fmt.Printf("Server returns %+v\n", resp) + return + } + } + + fmt.Println("File sent !") +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/.gitignore b/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/.gitignore new file mode 100644 index 00000000..b70bde05 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/.gitignore @@ -0,0 +1 @@ +hiplol diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/README.md b/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/README.md new file mode 100644 index 00000000..8455041b --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/README.md @@ -0,0 +1,11 @@ +lol +===== + +Prints the `(lol)` emoticon in the room in parameter. + +##### Usage + +```bash +go build +./hiplol --token= --room= +``` diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/main.go b/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/main.go new file mode 100644 index 00000000..d500ae9c --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/tbruyelle/hipchat-go/hipchat" +) + +var ( + token = flag.String("token", "", "The HipChat AuthToken") + roomId = flag.String("room", "", "The HipChat room id") + test = flag.Bool("t", false, "Enable auth_test parameter") +) + +func main() { + flag.Parse() + if *token == "" || *roomId == "" { + flag.PrintDefaults() + return + } + hipchat.AuthTest = *test + + c := hipchat.NewClient(*token) + + notifRq := &hipchat.NotificationRequest{Message: "Hey there!"} + + resp, err := c.Room.Notification(*roomId, notifRq) + if err != nil { + fmt.Fprintf(os.Stderr, "Error during room notification %q\n", err) + fmt.Fprintf(os.Stderr, "Server returns %+v\n", resp) + return + } + + if hipchat.AuthTest { + _, ok := hipchat.AuthTestResponse["success"] + fmt.Println("Authentification succeed :", ok) + } else { + fmt.Println("Lol sent !") + } +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/.gitignore b/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/.gitignore new file mode 100644 index 00000000..9ad9e2c8 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/.gitignore @@ -0,0 +1 @@ +hipshow diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/README.md b/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/README.md new file mode 100644 index 00000000..e91be606 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/README.md @@ -0,0 +1,11 @@ +hipshow +===== + +Prints all rooms from your group + +##### Usage + +```bash +go build +./hipshow --token= +``` diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/main.go b/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/main.go new file mode 100644 index 00000000..38476dee --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/tbruyelle/hipchat-go/hipchat" +) + +var ( + token = flag.String("token", "", "The HipChat AuthToken") + maxResults = flag.Int("maxResults", 5, "Max results per request") + includePrivate = flag.Bool("includePrivate", false, "Include private rooms?") + includeArchived = flag.Bool("includeArchived", false, "Include archived rooms?") +) + +func main() { + flag.Parse() + if *token == "" { + flag.PrintDefaults() + return + } + c := hipchat.NewClient(*token) + startIndex := 0 + totalRequests := 0 + var allRooms []hipchat.Room + + for { + opt := &hipchat.RoomsListOptions{ + ListOptions: hipchat.ListOptions{StartIndex: startIndex, MaxResults: *maxResults}, + IncludePrivate: *includePrivate, + IncludeArchived: *includeArchived} + + rooms, resp, err := c.Room.List(opt) + + if err != nil { + fmt.Printf("Error during room list req %q\n", err) + fmt.Printf("Server returns %+v\n", resp) + return + } + + totalRequests++ + + allRooms = append(allRooms, rooms.Items...) + if rooms.Links.Next != "" { + startIndex += *maxResults + } else { + break + } + } + + fmt.Printf("Your group has %d rooms, it took %d requests to retrieve all of them:\n", + len(allRooms), totalRequests) + for _, r := range allRooms { + fmt.Printf("%d %s \n", r.ID, r.Name) + } +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/.gitignore b/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/.gitignore new file mode 100644 index 00000000..5ec375c7 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/.gitignore @@ -0,0 +1 @@ +hiptail diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/README.md b/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/README.md new file mode 100644 index 00000000..6031abd2 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/README.md @@ -0,0 +1,11 @@ +hiptail +===== + +Prints recent messages in the room in parameter. + +##### Usage + +```bash +go build +./hiptail --token= --room= +``` diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/main.go b/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/main.go new file mode 100644 index 00000000..dd6eebed --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "flag" + "fmt" + "strings" + + "github.com/tbruyelle/hipchat-go/hipchat" +) + +const ( + maxMsgLen = 128 + moreString = " [MORE]" +) + +var ( + token = flag.String("token", "", "The HipChat AuthToken") + roomId = flag.String("room", "", "The HipChat room id") +) + +func main() { + flag.Parse() + if *token == "" || *roomId == "" { + flag.PrintDefaults() + return + } + c := hipchat.NewClient(*token) + hist, resp, err := c.Room.History(*roomId, &hipchat.HistoryOptions{}) + if err != nil { + fmt.Printf("Error during room history req %q\n", err) + fmt.Printf("Server returns %+v\n", resp) + return + } + for _, m := range hist.Items { + from := "" + switch m.From.(type) { + case string: + from = m.From.(string) + case map[string]interface{}: + f := m.From.(map[string]interface{}) + from = f["name"].(string) + } + msg := m.Message + if len(m.Message) > (maxMsgLen - len(moreString)) { + msg = fmt.Sprintf("%s%s", strings.Replace(m.Message[:len(m.Message)], "\n", " - ", -1), moreString) + } + fmt.Printf("%s [%s]: %s\n", from, m.Date, msg) + } +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/.gitignore b/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/.gitignore new file mode 100644 index 00000000..dd5f0150 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/.gitignore @@ -0,0 +1 @@ +hipwebhooks diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/README.md b/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/README.md new file mode 100644 index 00000000..90f0d2eb --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/README.md @@ -0,0 +1,21 @@ +Get a list of the currently active webhooks for each room or for a specific room + +List all rooms: + + $ go run main.go -token="$TOKEN" + +List a specific room: + + $ go run main.go -token="$TOKEN" -room="$ROOM" + +Delete a webhook: + + $ go run main.go -token="$TOKEN" -room="$ROOM" -action="delete" -webhook="$WEBHOOK" + +Create a webhook: + + $ go run main.go -token="$TOKEN" -room="$ROOM" -action="delete" \ + -name="$NAME" \ + -event="$EVENT" \ + -pattern="$PATTERN" \ + -url="$URL" diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/main.go b/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/main.go new file mode 100644 index 00000000..6a612e85 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "net/http" + + "github.com/tbruyelle/hipchat-go/hipchat" +) + +var ( + token = flag.String("token", "", "The HipChat AuthToken") + roomId = flag.String("room", "", "A specific Room ID") + action = flag.String("action", "", "An action to take. Currently supported: create, delete") + + // delete + webhookId = flag.String("webhook", "", "A specific Room ID") + + // create + name = flag.String("name", "", "With action: create, name for the new webhook") + event = flag.String("event", "", "With action: create, event for the new webhook (enter, exit, message, notification, topic_change)") + pattern = flag.String("pattern", "", "With action: create, pattern for the new webhook") + url = flag.String("url", "", "With action: create, target URL for the new webhook") +) + +func main() { + flag.Parse() + if *token == "" { + flag.PrintDefaults() + return + } + c := hipchat.NewClient(*token) + + if *action == "" { + if *roomId == "" { + // If no room is given, look up all rooms and all of their webhooks + rooms, resp, err := c.Room.List(&hipchat.RoomsListOptions{}) + handleRequestError(resp, err) + + for _, room := range rooms.Items { + fmt.Printf("%-25v%10v\n", room.Name, room.ID) + + hooks, resp, err := c.Room.ListWebhooks(room.ID, nil) + handleRequestError(resp, err) + + for _, webhook := range hooks.Webhooks { + fmt.Printf(" %v %v\t%v\t%v\t%v\n", webhook.Name, webhook.ID, webhook.Event, webhook.URL, webhook.Links.Self) + } + + fmt.Println("---") + } + } else { + // If room is given, just get the webhooks for that room + hooks, resp, err := c.Room.ListWebhooks(*roomId, nil) + handleRequestError(resp, err) + + for _, webhook := range hooks.Webhooks { + fmt.Printf(" %v %v\t%v\t%v\t%v\n", webhook.Name, webhook.ID, webhook.Event, webhook.URL, webhook.Links.Self) + } + } + } else if *action == "create" { + if *roomId == "" { + fmt.Println("roomId is required for webhook creation") + flag.PrintDefaults() + return + } + + webhook, resp, err := c.Room.CreateWebhook(*roomId, &hipchat.CreateWebhookRequest{ + Name: *name, + Event: "room_" + *event, + Pattern: *pattern, + URL: *url, + }) + handleRequestError(resp, err) + fmt.Printf("%v\t%v\t%v\t%v\n", webhook.Name, webhook.Event, webhook.URL, webhook.Links.Self) + } else if *action == "delete" { + if *roomId == "" { + fmt.Println("roomId is required for webhook deletion") + flag.PrintDefaults() + return + } + + if *webhookId == "" { + fmt.Println("webhookId is required for webhook deletion") + flag.PrintDefaults() + return + } + + resp, err := c.Room.DeleteWebhook(*roomId, *webhookId) + handleRequestError(resp, err) + + fmt.Println("Deleted webhook with id", *webhookId) + } + +} + +func handleRequestError(resp *http.Response, err error) { + if err != nil { + if resp != nil { + fmt.Printf("Request Failed:\n%+v\n", resp) + body, _ := ioutil.ReadAll(resp.Body) + fmt.Printf("%+v\n", body) + } else { + fmt.Printf("Request failed, response is nil") + } + panic(err) + } +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/emoticon.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/emoticon.go new file mode 100644 index 00000000..a8d2539a --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/emoticon.go @@ -0,0 +1,52 @@ +package hipchat + +import ( + "net/http" +) + +// EmoticonService gives access to the emoticon related part of the API. +type EmoticonService struct { + client *Client +} + +// Emoticons represents a list of hipchat emoticons. +type Emoticons struct { + Items []Emoticon `json:"items"` + StartIndex int `json:"startIndex"` + MaxResults int `json:"maxResults"` + Links PageLinks `json:"links"` +} + +// Emoticon represents a hipchat emoticon. +type Emoticon struct { + ID int `json:"id"` + URL string `json:"url"` + Links Links `json:"links"` + Shortcut string `json:"shortcut"` +} + +// EmoticonsListOptions specifies the optionnal parameters of the EmoticonService.List +// method. +type EmoticonsListOptions struct { + ListOptions + + // The type of emoticons to get (global, group or all) + Type string `url:"type,omitempty"` +} + +// List returns the list of all the emoticons +// +// HipChat api docs : https://www.hipchat.com/docs/apiv2/method/get_all_emoticons +func (e *EmoticonService) List(opt *EmoticonsListOptions) (*Emoticons, *http.Response, error) { + req, err := e.client.NewRequest("GET", "emoticon", opt, nil) + if err != nil { + return nil, nil, err + } + + emoticons := new(Emoticons) + resp, err := e.client.Do(req, emoticons) + if err != nil { + return nil, resp, err + } + return emoticons, resp, nil +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/emoticon_test.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/emoticon_test.go new file mode 100644 index 00000000..aeef06a8 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/emoticon_test.go @@ -0,0 +1,43 @@ +package hipchat + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestEmoticonList(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/emoticon", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{ + "start-index": "1", + "max-results": "100", + "type": "type", + }) + fmt.Fprintf(w, `{ + "items": [{"id":1, "url":"u", "shortcut":"s", "links":{"self":"s"}}], + "startIndex": 1, + "maxResults": 1, + "links":{"self":"s", "prev":"p", "next":"n"} + }`) + }) + want := &Emoticons{ + Items: []Emoticon{{ID: 1, URL: "u", Shortcut: "s", Links: Links{Self: "s"}}}, + StartIndex: 1, + MaxResults: 1, + Links: PageLinks{Links: Links{Self: "s"}, Prev: "p", Next: "n"}, + } + + opt := &EmoticonsListOptions{ListOptions{1, 100}, "type"} + emos, _, err := client.Emoticon.List(opt) + if err != nil { + t.Fatalf("Emoticon.List returned an error %v", err) + } + if !reflect.DeepEqual(want, emos) { + t.Errorf("Emoticon.List returned %+v, want %+v", emos, want) + } +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat.go new file mode 100644 index 00000000..bd1288ff --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat.go @@ -0,0 +1,435 @@ +// Package hipchat provides a client for using the HipChat API v2. +package hipchat + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "math/rand" + "mime" + "net/http" + "net/url" + "os" + "os/user" + "path/filepath" + "reflect" + "strconv" + "strings" + "time" + + "github.com/google/go-querystring/query" +) + +const ( + defaultBaseURL = "https://api.hipchat.com/v2/" +) + +// HTTPClient is an interface that allows overriding the http behavior +// by providing custom http clients +type HTTPClient interface { + Do(req *http.Request) (res *http.Response, err error) +} + +// LimitData contains the latest Rate Limit or Flood Control data sent with every API call response. +// +// Limit is the number of API calls per period of time +// Remaining is the current number of API calls that can be done before the ResetTime +// ResetTime is the UTC time in Unix epoch format for when the full Limit of API calls will be restored. +type LimitData struct { + Limit int + Remaining int + ResetTime int +} + +// Client manages the communication with the HipChat API. +// +// LatestFloodControl contains the response from the latest API call's response headers X-Floodcontrol-{Limit, Remaining, ResetTime} +// LatestRateLimit contains the response from the latest API call's response headers X-Ratelimit-{Limit, Remaining, ResetTime} +// Room gives access to the /room part of the API. +// User gives access to the /user part of the API. +// Emoticon gives access to the /emoticon part of the API. +type Client struct { + authToken string + BaseURL *url.URL + client HTTPClient + LatestFloodControl LimitData + LatestRateLimit LimitData + Room *RoomService + User *UserService + Emoticon *EmoticonService +} + +// Links represents the HipChat default links. +type Links struct { + Self string `json:"self"` +} + +// PageLinks represents the HipChat page links. +type PageLinks struct { + Links + Prev string `json:"prev"` + Next string `json:"next"` +} + +// ID represents a HipChat id. +// Use a separate struct because it can be a string or a int. +type ID struct { + ID string `json:"id"` +} + +// ListOptions specifies the optional parameters to various List methods that +// support pagination. +// +// For paginated results, StartIndex represents the first page to display. +// For paginated results, MaxResults reprensents the number of items per page. Default value is 100. Maximum value is 1000. +type ListOptions struct { + StartIndex int `url:"start-index,omitempty"` + MaxResults int `url:"max-results,omitempty"` +} + +// ExpandOptions specifies which Hipchat collections to automatically expand. +// This functionality is primarily used to reduce the total time to receive the data. +// It also reduces the sheer number of API calls from 1+N, to 1. +// +// cf: https://developer.atlassian.com/hipchat/guide/hipchat-rest-api/api-title-expansion +type ExpandOptions struct { + Expand string `url:"expand,omitempty"` +} + +// Color is set of hard-coded string values for the HipChat API for notifications. +// cf: https://www.hipchat.com/docs/apiv2/method/send_room_notification +type Color string + +const ( + // ColorYellow is the color yellow + ColorYellow Color = "yellow" + // ColorGreen is the color green + ColorGreen Color = "green" + // ColorRed is the color red + ColorRed Color = "red" + // ColorPurple is the color purple + ColorPurple Color = "purple" + // ColorGray is the color gray + ColorGray Color = "gray" + // ColorRandom is the random "surprise me!" color + ColorRandom Color = "random" +) + +// AuthTest can be set to true to test an auth token. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/auth#auth_test +var AuthTest = false + +// AuthTestResponse will contain the server response of any +// API calls if AuthTest=true. +var AuthTestResponse = map[string]interface{}{} + +// RetryOnRateLimit can be set to true to automatically retry the API call until it succeeds, +// subject to the RateLimitRetryPolicy settings. This behavior is only active when the API +// call returns 429 (StatusTooManyRequests). +var RetryOnRateLimit = false + +// RetryPolicy defines a RetryPolicy. +// +// MaxRetries is the maximum number of attempts to make before returning an error +// MinDelay is the initial delay between attempts. This value is multiplied by the current attempt number. +// MaxDelay is the largest delay between attempts. +// JitterDelay is the amount of random jitter to add to the delay. +// JitterBias is the amount of jitter to remove from the delay. +// +// The use of Jitter avoids inadvertant and undesirable synchronization of network +// operations between otherwise unrelated clients. +// cf: https://brooker.co.za/blog/2015/03/21/backoff.html and https://www.awsarchitectureblog.com/2015/03/backoff.html +// +// Using the values of JitterDelay = 250 milliseconds and a JitterBias of negative 125 milliseconds, +// would result in a uniformly distributed Jitter between -125 and +125 milliseconds, centered +// around the current trial Delay (between MinDelay and MaxDelay). +// +// +type RetryPolicy struct { + MaxRetries int + MinDelay time.Duration + MaxDelay time.Duration + JitterDelay time.Duration + JitterBias time.Duration +} + +// NoRateLimitRetryPolicy defines the "never retry an API call" policy's values. +var NoRateLimitRetryPolicy = RetryPolicy{0, 1 * time.Second, 1 * time.Second, 500 * time.Millisecond, 0 * time.Millisecond} + +// DefaultRateLimitRetryPolicy defines the "up to 300 times, 1 second apart, randomly adding an additional up-to-500 milliseconds of delay" policy. +var DefaultRateLimitRetryPolicy = RetryPolicy{300, 1 * time.Second, 1 * time.Second, 500 * time.Millisecond, 0 * time.Millisecond} + +// RateLimitRetryPolicy can be set to a custom RetryPolicy's values, +// or to one of the two predefined ones: NoRateLimitRetryPolicy or DefaultRateLimitRetryPolicy +var RateLimitRetryPolicy = DefaultRateLimitRetryPolicy + +// NewClient returns a new HipChat API client. You must provide a valid +// AuthToken retrieved from your HipChat account. +func NewClient(authToken string) *Client { + baseURL, err := url.Parse(defaultBaseURL) + if err != nil { + panic(err) + } + + c := &Client{ + authToken: authToken, + BaseURL: baseURL, + client: http.DefaultClient, + } + c.Room = &RoomService{client: c} + c.User = &UserService{client: c} + c.Emoticon = &EmoticonService{client: c} + return c +} + +// SetHTTPClient sets the http client for performing API requests. +// This method allows overriding the default http client with any +// implementation of the HTTPClient interface. It is typically used +// to have finer control of the http request. +// If a nil httpClient is provided, http.DefaultClient will be used. +func (c *Client) SetHTTPClient(httpClient HTTPClient) { + if httpClient == nil { + c.client = http.DefaultClient + } else { + c.client = httpClient + } +} + +// NewRequest creates an API request. This method can be used to performs +// API request not implemented in this library. Otherwise it should not be +// be used directly. +// Relative URLs should always be specified without a preceding slash. +func (c *Client) NewRequest(method, urlStr string, opt interface{}, body interface{}) (*http.Request, error) { + rel, err := addOptions(urlStr, opt) + if err != nil { + return nil, err + } + + if AuthTest { + // Add the auth_test param + values := rel.Query() + values.Add("auth_test", strconv.FormatBool(AuthTest)) + rel.RawQuery = values.Encode() + } + + u := c.BaseURL.ResolveReference(rel) + + buf := new(bytes.Buffer) + if body != nil { + err := json.NewEncoder(buf).Encode(body) + if err != nil { + return nil, err + } + } + + req, err := http.NewRequest(method, u.String(), buf) + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", "Bearer "+c.authToken) + req.Header.Add("Content-Type", "application/json") + return req, nil +} + +// NewFileUploadRequest creates an API request to upload a file. +// This method manually formats the request as multipart/related with a single part +// of content-type application/json and a second part containing the file to be sent. +// Relative URLs should always be specified without a preceding slash. +func (c *Client) NewFileUploadRequest(method, urlStr string, v interface{}) (*http.Request, error) { + rel, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + + u := c.BaseURL.ResolveReference(rel) + + shareFileReq, ok := v.(*ShareFileRequest) + if !ok { + return nil, errors.New("ShareFileRequest corrupted") + } + path := shareFileReq.Path + message := shareFileReq.Message + + // Resolve home path + if strings.HasPrefix(path, "~") { + usr, _ := user.Current() + path = strings.Replace(path, "~", usr.HomeDir, 1) + } + + // Check if file exists + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil, err + } + + // Read file and encode to base 64 + file, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + b64 := base64.StdEncoding.EncodeToString(file) + contentType := mime.TypeByExtension(filepath.Ext(path)) + + // Set proper filename + filename := shareFileReq.Filename + if filename == "" { + filename = filepath.Base(path) + } else if filepath.Ext(filename) != filepath.Ext(path) { + filename = filepath.Base(filename) + filepath.Ext(path) + } + + // Build request body + body := "--hipfileboundary\n" + + "Content-Type: application/json; charset=UTF-8\n" + + "Content-Disposition: attachment; name=\"metadata\"\n\n" + + "{\"message\": \"" + message + "\"}\n" + + "--hipfileboundary\n" + + "Content-Type: " + contentType + " charset=UTF-8\n" + + "Content-Transfer-Encoding: base64\n" + + "Content-Disposition: attachment; name=file; filename=" + filename + "\n\n" + + b64 + "\n" + + "--hipfileboundary\n" + + b := &bytes.Buffer{} + _, err = b.Write([]byte(body)) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(method, u.String(), b) + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", "Bearer "+c.authToken) + req.Header.Add("Content-Type", "multipart/related; boundary=hipfileboundary") + + return req, err +} + +// Do performs the request, the json received in the response is decoded +// and stored in the value pointed by v. +// Do can be used to perform the request created with NewRequest, which +// should be used only for API requests not implemented in this library. +func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { + var policy = NoRateLimitRetryPolicy + if RetryOnRateLimit { + policy = RateLimitRetryPolicy + } + + resp, err := c.doWithRetryPolicy(req, policy) + if err != nil { + return nil, err + } + + if AuthTest { + // If AuthTest is enabled, the reponse won't be the + // one defined in the API endpoint. + err = json.NewDecoder(resp.Body).Decode(&AuthTestResponse) + } else { + if c := resp.StatusCode; c < 200 || c > 299 { + return resp, fmt.Errorf("Server returns status %d", c) + } + + if v != nil { + defer resp.Body.Close() + if w, ok := v.(io.Writer); ok { + _, err = io.Copy(w, resp.Body) + } else { + err = json.NewDecoder(resp.Body).Decode(v) + } + } + } + return resp, err +} + +func (c *Client) doWithRetryPolicy(req *http.Request, policy RetryPolicy) (*http.Response, error) { + currentTry := 0 + + for willContinue(currentTry, policy) { + currentTry = currentTry + 1 + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + c.captureRateLimits(resp) + if http.StatusTooManyRequests == resp.StatusCode { + resp.Body.Close() + if willContinue(currentTry, policy) { + sleep(currentTry, policy) + } + } else { + return resp, nil + } + } + return nil, fmt.Errorf("max retries exceeded (%d)", policy.MaxRetries) +} + +func willContinue(currentTry int, policy RetryPolicy) bool { + return currentTry <= policy.MaxRetries +} + +func sleep(currentTry int, policy RetryPolicy) { + jitter := time.Duration(rand.Int63n(2*int64(policy.JitterDelay))) - policy.JitterBias + linearDelay := time.Duration(currentTry)*policy.MinDelay + jitter + if linearDelay > policy.MaxDelay { + linearDelay = policy.MaxDelay + } + time.Sleep(time.Duration(linearDelay)) +} + +func setIfPresent(src string, dest *int) { + if len(src) > 0 { + v, err := strconv.Atoi(src) + if err != nil { + *dest = v + } + } +} + +func (c *Client) captureRateLimits(resp *http.Response) { + // BY DESIGN: + // if and only if the HTTP Response headers contain the header are the values updated. + // The Floodcontrol limits are orthogonal to the API limits. + // API Limits are consumed for each and every API call. + // The default value for API limits are 500 (app token) or 100 (user token). + // Flood Control limits are consumed only when a user message, room message, or room notification is sent. + // The default value for Flood Control limits is 30 per minute per user token. + setIfPresent(resp.Header.Get("X-Ratelimit-Limit"), &c.LatestRateLimit.Limit) + setIfPresent(resp.Header.Get("X-Ratelimit-Remaining"), &c.LatestRateLimit.Remaining) + setIfPresent(resp.Header.Get("X-Ratelimit-Reset"), &c.LatestRateLimit.ResetTime) + setIfPresent(resp.Header.Get("X-Floodcontrol-Limit"), &c.LatestFloodControl.Limit) + setIfPresent(resp.Header.Get("X-Floodcontrol-Remaining"), &c.LatestFloodControl.Remaining) + setIfPresent(resp.Header.Get("X-Floodcontrol-Reset"), &c.LatestFloodControl.ResetTime) +} + +// addOptions adds the parameters in opt as URL query parameters to s. opt +// must be a struct whose fields may contain "url" tags. +func addOptions(s string, opt interface{}) (*url.URL, error) { + u, err := url.Parse(s) + if err != nil { + return nil, err + } + if opt == nil { + return u, nil + } + + v := reflect.ValueOf(opt) + if v.Kind() == reflect.Ptr && v.IsNil() { + // No query string to add + return u, nil + } + + qs, err := query.Values(opt) + if err != nil { + return nil, err + } + + u.RawQuery = qs.Encode() + return u, nil +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat_test.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat_test.go new file mode 100644 index 00000000..52383022 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat_test.go @@ -0,0 +1,210 @@ +package hipchat + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" +) + +var ( + mux *http.ServeMux + server *httptest.Server + client *Client +) + +// setup sets up a test HTTP server and a hipchat.Client configured to talk +// to that test server. +// Tests should register handlers on mux which provide mock responses for +// the API method being tested. +func setup() { + // test server + mux = http.NewServeMux() + server = httptest.NewServer(mux) + + // github client configured to use test server + client = NewClient("AuthToken") + url, _ := url.Parse(server.URL) + client.BaseURL = url +} + +// teardown closes the test HTTP server. +func teardown() { + server.Close() +} + +func testMethod(t *testing.T, r *http.Request, want string) { + if got := r.Method; got != want { + t.Errorf("Request method: %v, want %v", got, want) + } +} + +type values map[string]string + +func testFormValues(t *testing.T, r *http.Request, values values) { + want := url.Values{} + for k, v := range values { + want.Add(k, v) + } + + r.ParseForm() + if got := r.Form; !reflect.DeepEqual(got, want) { + t.Errorf("Request parameters: %v, want %v", got, want) + } +} + +func testHeader(t *testing.T, r *http.Request, header string, want string) { + if got := r.Header.Get(header); got != want { + t.Errorf("Header.Get(%q) returned %s, want %s", header, got, want) + } +} + +func TestNewClient(t *testing.T) { + authToken := "AuthToken" + + c := NewClient(authToken) + + if c.authToken != authToken { + t.Errorf("NewClient authToken %s, want %s", c.authToken, authToken) + } + if c.BaseURL.String() != defaultBaseURL { + t.Errorf("NewClient BaseURL %s, want %s", c.BaseURL.String(), defaultBaseURL) + } + if c.client != http.DefaultClient { + t.Errorf("SetHTTPClient client %v, want %p", c.client, http.DefaultClient) + } +} + +func TestSetHTTPClient(t *testing.T) { + c := NewClient("AuthToken") + + httpClient := new(http.Client) + c.SetHTTPClient(httpClient) + + if c.client != httpClient { + t.Errorf("SetHTTPClient client %v, want %p", c.client, httpClient) + } +} + +type customHTTPClient struct{} + +func (c customHTTPClient) Do(*http.Request) (*http.Response, error) { + return nil, nil +} + +func TestSetCustomHTTPClient(t *testing.T) { + c := NewClient("AuthToken") + + httpClient := new(customHTTPClient) + c.SetHTTPClient(httpClient) + + if c.client != httpClient { + t.Errorf("SetHTTPClient client %v, want %p", c.client, httpClient) + } +} + +func TestSetHTTPClient_NilHTTPClient(t *testing.T) { + c := NewClient("AuthToken") + + c.SetHTTPClient(nil) + + if c.client != http.DefaultClient { + t.Errorf("SetHTTPClient client %v, want %p", c.client, http.DefaultClient) + } +} + +func TestNewRequest(t *testing.T) { + c := NewClient("AuthToken") + + inURL, outURL := "foo", defaultBaseURL+"foo?max-results=100&start-index=1" + opt := &ListOptions{StartIndex: 1, MaxResults: 100} + inBody, outBody := &NotificationRequest{Message: "Hello"}, `{"message":"Hello"}`+"\n" + r, _ := c.NewRequest("GET", inURL, opt, inBody) + + if r.URL.String() != outURL { + t.Errorf("NewRequest URL %s, want %s", r.URL.String(), outURL) + } + body, _ := ioutil.ReadAll(r.Body) + if string(body) != outBody { + t.Errorf("NewRequest body %s, want %s", body, outBody) + } + authorization := r.Header.Get("Authorization") + if authorization != "Bearer "+c.authToken { + t.Errorf("NewRequest authorization header %s, want %s", authorization, "Bearer "+c.authToken) + } + contentType := r.Header.Get("Content-Type") + if contentType != "application/json" { + t.Errorf("NewRequest Content-Type header %s, want application/json", contentType) + } +} + +func TestNewRequest_AuthTestEnabled(t *testing.T) { + AuthTest = true + defer func() { AuthTest = false }() + c := NewClient("AuthToken") + + inURL, outURL := "foo", defaultBaseURL+"foo?auth_test=true" + r, _ := c.NewRequest("GET", inURL, nil, nil) + + if r.URL.String() != outURL { + t.Errorf("NewRequest URL %s, want %s", r.URL.String(), outURL) + } +} + +func TestDo(t *testing.T) { + setup() + defer teardown() + + type foo struct { + Bar int + } + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if m := "GET"; m != r.Method { + t.Errorf("Request method = %v, want %v", r.Method, m) + } + fmt.Fprintf(w, `{"Bar":1}`) + }) + req, _ := client.NewRequest("GET", "/", nil, nil) + body := new(foo) + + _, err := client.Do(req, body) + + if err != nil { + t.Fatal(err) + } + want := &foo{Bar: 1} + if !reflect.DeepEqual(body, want) { + t.Errorf("Response body = %v, want %v", body, want) + } +} + +func TestDo_AuthTestEnabled(t *testing.T) { + AuthTest = true + defer func() { AuthTest = false }() + setup() + defer teardown() + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if m := "GET"; m != r.Method { + t.Errorf("Request method = %v, want %v", r.Method, m) + } + if r.URL.Query().Get("auth_test") == "true" { + fmt.Fprintf(w, `{"success":{ "code": 202, "type": "Accepted", "message": "This auth_token has access to use this method." }}`) + } else { + fmt.Fprintf(w, `{"Bar":1}`) + } + }) + req, _ := client.NewRequest("GET", "/", nil, nil) + + _, err := client.Do(req, nil) + + if err != nil { + t.Fatal(err) + } + if _, ok := AuthTestResponse["success"]; !ok { + t.Errorf("Response body = %v, want succeed", AuthTestResponse) + } +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth.go new file mode 100644 index 00000000..7211ba80 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth.go @@ -0,0 +1,107 @@ +package hipchat + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +// ClientCredentials represents the OAuth2 client ID and secret for an integration +type ClientCredentials struct { + ClientID string + ClientSecret string +} + +// OAuthAccessToken represents a newly created Hipchat OAuth access token +type OAuthAccessToken struct { + AccessToken string `json:"access_token"` + ExpiresIn uint32 `json:"expires_in"` + GroupID uint32 `json:"group_id"` + GroupName string `json:"group_name"` + Scope string `json:"scope"` + TokenType string `json:"token_type"` +} + +// CreateClient creates a new client from this OAuth token +func (t *OAuthAccessToken) CreateClient() *Client { + return NewClient(t.AccessToken) +} + +// GenerateToken returns back an access token for a given integration's client ID and client secret +// +// HipChat API documentation: https://www.hipchat.com/docs/apiv2/method/generate_token +func (c *Client) GenerateToken(credentials ClientCredentials, scopes []string) (*OAuthAccessToken, *http.Response, error) { + rel, err := url.Parse("oauth/token") + + if err != nil { + return nil, nil, err + } + + u := c.BaseURL.ResolveReference(rel) + + params := url.Values{"grant_type": {"client_credentials"}, + "scope": {strings.Join(scopes, " ")}} + req, err := http.NewRequest("POST", u.String(), strings.NewReader(params.Encode())) + + if err != nil { + return nil, nil, err + } + + req.SetBasicAuth(credentials.ClientID, credentials.ClientSecret) + req.Header.Set("Content-type", "application/x-www-form-urlencoded") + + resp, err := c.client.Do(req) + + if err != nil { + return nil, resp, err + } + + if resp.StatusCode != 200 { + content, readerr := ioutil.ReadAll(resp.Body) + + if readerr != nil { + content = []byte("Unknown error") + } + + return nil, resp, fmt.Errorf("Couldn't retrieve access token: %s", content) + } + + content, err := ioutil.ReadAll(resp.Body) + + var token OAuthAccessToken + err = json.Unmarshal(content, &token) + + return &token, resp, err +} + +const ( + // ScopeAdminGroup - Perform group administrative tasks + ScopeAdminGroup = "admin_group" + + // ScopeAdminRoom - Perform room administrative tasks + ScopeAdminRoom = "admin_room" + + // ScopeImportData - Import users, rooms, and chat history. Only available for select add-ons. + ScopeImportData = "import_data" + + // ScopeManageRooms - Create, update, and remove rooms + ScopeManageRooms = "manage_rooms" + + // ScopeSendMessage - Send private one-on-one messages + ScopeSendMessage = "send_message" + + // ScopeSendNotification - Send room notifications + ScopeSendNotification = "send_notification" + + // ScopeViewGroup - View users, rooms, and other group information + ScopeViewGroup = "view_group" + + // ScopeViewMessages - View messages from chat rooms and private chats you have access to + ScopeViewMessages = "view_messages" + + // ScopeViewRoom - View room information and participants, but not history + ScopeViewRoom = "view_room" +) diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth_test.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth_test.go new file mode 100644 index 00000000..f9bbac8c --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth_test.go @@ -0,0 +1,79 @@ +package hipchat + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestGetAccessToken(t *testing.T) { + setup() + defer teardown() + + clientID := "client-abcdef" + clientSecret := "secret-12345" + + mux.HandleFunc("/oauth/token", func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() != "/oauth/token" { + t.Errorf("Incorrect URL = %v, want %v", r.URL, "/oauth/token") + } + + testMethod(t, r, "POST") + testHeader(t, r, "Authorization", "Basic Y2xpZW50LWFiY2RlZjpzZWNyZXQtMTIzNDU=") + testFormValues(t, r, values{ + + "grant_type": "client_credentials", + "scope": "send_notification view_room", + }) + fmt.Fprintf(w, ` + { + "access_token": "q0M8p3UrBL96uHb79x4qdR2r6oEnCeajcg123456", + "expires_in": 3599, + "group_id": 123456, + "group_name": "TestGroup", + "scope": "send_notification view_room", + "token_type": "bearer" + } + `) + }) + want := &OAuthAccessToken{ + AccessToken: "q0M8p3UrBL96uHb79x4qdR2r6oEnCeajcg123456", + ExpiresIn: 3599, + GroupID: 123456, + GroupName: "TestGroup", + Scope: "send_notification view_room", + TokenType: "bearer", + } + + credentials := ClientCredentials{ClientID: clientID, ClientSecret: clientSecret} + + token, _, err := client.GenerateToken(credentials, []string{ScopeSendNotification, ScopeViewRoom}) + if err != nil { + t.Fatalf("Client.GetAccessToken returns an error %v", err) + } + if !reflect.DeepEqual(want, token) { + t.Errorf("Client.GetAccessToken returned %+v, want %+v", token, want) + } +} + +func TestCreateClientFromAccessToken(t *testing.T) { + token := OAuthAccessToken{ + AccessToken: "q0M8p3UrBL96uHb79x4qdR2r6oEnCeajcg123456", + ExpiresIn: 3599, + GroupID: 123456, + GroupName: "TestGroup", + Scope: "send_notification view_room", + TokenType: "bearer", + } + + client := token.CreateClient() + + if client.authToken != token.AccessToken { + t.Fatalf( + "Client auth token does not match access token: %v != %v", + client.authToken, + token.AccessToken, + ) + } +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/room.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room.go new file mode 100644 index 00000000..50e1a6d6 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room.go @@ -0,0 +1,659 @@ +package hipchat + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +// RoomService gives access to the room related methods of the API. +type RoomService struct { + client *Client +} + +// Rooms represents a HipChat room list. +type Rooms struct { + Items []Room `json:"items"` + StartIndex int `json:"startIndex"` + MaxResults int `json:"maxResults"` + Links PageLinks `json:"links"` +} + +// Room represents a HipChat room. +type Room struct { + ID int `json:"id"` + Links RoomLinks `json:"links"` + Name string `json:"name"` + XMPPJid string `json:"xmpp_jid"` + Statistics RoomStatistics `json:"statistics"` + Created string `json:"created"` + IsArchived bool `json:"is_archived"` + Privacy string `json:"privacy"` + IsGuestAccessible bool `json:"is_guess_accessible"` + Topic string `json:"topic"` + Participants []User `json:"participants"` + Owner User `json:"owner"` + GuestAccessURL string `json:"guest_access_url"` +} + +// RoomStatistics represents the HipChat room statistics. +type RoomStatistics struct { + Links Links `json:"links"` + MessagesSent int `json:"messages_sent,omitempty"` + LastActive string `json:"last_active,omitempty"` +} + +// CreateRoomRequest represents a HipChat room creation request. +type CreateRoomRequest struct { + Topic string `json:"topic,omitempty"` + GuestAccess bool `json:"guest_access,omitempty"` + Name string `json:"name,omitempty"` + OwnerUserID string `json:"owner_user_id,omitempty"` + Privacy string `json:"privacy,omitempty"` +} + +// UpdateRoomRequest represents a HipChat room update request. +type UpdateRoomRequest struct { + Name string `json:"name"` + Topic string `json:"topic"` + IsGuestAccess bool `json:"is_guest_accessible"` + IsArchived bool `json:"is_archived"` + Privacy string `json:"privacy"` + Owner ID `json:"owner"` +} + +// RoomLinks represents the HipChat room links. +type RoomLinks struct { + Links + Webhooks string `json:"webhooks"` + Members string `json:"members"` + Participants string `json:"participants"` +} + +// NotificationRequest represents a HipChat room notification request. +type NotificationRequest struct { + Color Color `json:"color,omitempty"` + Message string `json:"message,omitempty"` + Notify bool `json:"notify,omitempty"` + MessageFormat string `json:"message_format,omitempty"` + From string `json:"from,omitempty"` + Card *Card `json:"card,omitempty"` +} + +// RoomMessageRequest represents a Hipchat room message request. +type RoomMessageRequest struct { + Message string `json:"message"` +} + +// Card is used to send information as messages to Hipchat rooms +type Card struct { + Style string `json:"style"` + Description CardDescription `json:"description"` + Format string `json:"format,omitempty"` + URL string `json:"url,omitempty"` + Title string `json:"title"` + Thumbnail *Thumbnail `json:"thumbnail,omitempty"` + Activity *Activity `json:"activity,omitempty"` + Attributes []Attribute `json:"attributes,omitempty"` + ID string `json:"id,omitempty"` + Icon *Icon `json:"icon,omitempty"` +} + +const ( + // CardStyleFile represents a Card notification related to a file + CardStyleFile = "file" + + // CardStyleImage represents a Card notification related to an image + CardStyleImage = "image" + + // CardStyleApplication represents a Card notification related to an application + CardStyleApplication = "application" + + // CardStyleLink represents a Card notification related to a link + CardStyleLink = "link" + + // CardStyleMedia represents a Card notiifcation related to media + CardStyleMedia = "media" +) + +// CardDescription represents the main content of the Card +type CardDescription struct { + Format string + Value string +} + +// MarshalJSON serializes a CardDescription into JSON +func (c CardDescription) MarshalJSON() ([]byte, error) { + if c.Format == "" { + return json.Marshal(c.Value) + } + + obj := make(map[string]string) + obj["format"] = c.Format + obj["value"] = c.Value + + return json.Marshal(obj) +} + +// UnmarshalJSON deserializes a JSON-serialized CardDescription +func (c *CardDescription) UnmarshalJSON(data []byte) error { + // Compact the JSON to make it easier to process below + buffer := bytes.NewBuffer([]byte{}) + err := json.Compact(buffer, data) + if err != nil { + return err + } + data = buffer.Bytes() + + // Since Description can be either a string value or an object, we + // must check and deserialize appropriately + + if data[0] == 123 { // == } + obj := make(map[string]string) + + err = json.Unmarshal(data, &obj) + if err != nil { + return err + } + + c.Format = obj["format"] + c.Value = obj["value"] + } else { + c.Format = "" + err = json.Unmarshal(data, &c.Value) + } + + if err != nil { + return err + } + + return nil +} + +// Icon represents an icon +type Icon struct { + URL string `json:"url"` + URL2x string `json:"url@2x,omitempty"` +} + +// Thumbnail represents a thumbnail image +type Thumbnail struct { + URL string `json:"url"` + URL2x string `json:"url@2x,omitempty"` + Width uint `json:"width,omitempty"` + Height uint `json:"height,omitempty"` +} + +// Attribute represents an attribute on a Card +type Attribute struct { + Label string `json:"label,omitempty"` + Value AttributeValue `json:"value"` +} + +// AttributeValue represents the value of an attribute +type AttributeValue struct { + URL string `json:"url,omitempty"` + Style string `json:"style,omitempty"` + Type string `json:"type,omitempty"` + Label string `json:"label,omitempty"` + Value string `json:"value,omitempty"` + Icon *Icon `json:"icon,omitempty"` +} + +// Activity represents an activity that occurred +type Activity struct { + Icon *Icon `json:"icon,omitempty"` + HTML string `json:"html,omitempty"` +} + +// ShareFileRequest represents a HipChat room file share request. +type ShareFileRequest struct { + Path string `json:"path"` + Filename string `json:"filename,omitempty"` + Message string `json:"message,omitempty"` +} + +// History represents a HipChat room chat history. +type History struct { + Items []Message `json:"items"` + StartIndex int `json:"startIndex"` + MaxResults int `json:"maxResults"` + Links PageLinks `json:"links"` +} + +// Message represents a HipChat message. +type Message struct { + Date string `json:"date"` + From interface{} `json:"from"` // string | obj <- weak + ID string `json:"id"` + Mentions []User `json:"mentions"` + Message string `json:"message"` + MessageFormat string `json:"message_format"` + Type string `json:"type"` +} + +// SetTopicRequest represents a hipchat update topic request +type SetTopicRequest struct { + Topic string `json:"topic"` +} + +// InviteRequest represents a hipchat invite to room request +type InviteRequest struct { + Reason string `json:"reason"` +} + +// AddMemberRequest represents a HipChat add member request +type AddMemberRequest struct { + Roles []string `json:"roles,omitempty"` +} + +// GlanceRequest represents a HipChat room ui glance +type GlanceRequest struct { + Key string `json:"key"` + Name GlanceName `json:"name"` + Target string `json:"target"` + QueryURL string `json:"queryUrl,omitempty"` + Icon Icon `json:"icon"` + Conditions []*GlanceCondition `json:"conditions,omitempty"` +} + +// GlanceName represents a glance name +type GlanceName struct { + Value string `json:"value"` + I18n string `json:"i18n,omitempty"` +} + +// GlanceCondition represents a condition to determine whether a glance is displayed +type GlanceCondition struct { + Condition string `json:"condition"` + Params map[string]string `json:"params"` + Invert bool `json:"invert"` +} + +// GlanceUpdateRequest represents a HipChat room ui glance update request +type GlanceUpdateRequest struct { + Glance []*GlanceUpdate `json:"glance"` +} + +// GlanceUpdate represents a component of a HipChat room ui glance update +type GlanceUpdate struct { + Key string `json:"key"` + Content GlanceContent `json:"content"` +} + +// GlanceContent is a component of a Glance +type GlanceContent struct { + Status *GlanceStatus `json:"status,omitempty"` + Metadata interface{} `json:"metadata,omitempty"` + Label AttributeValue `json:"label"` // AttributeValue{Type, Label} +} + +// GlanceStatus is a status field component of a GlanceContent +type GlanceStatus struct { + Type string `json:"type"` // "lozenge" | "icon" + Value interface{} `json:"value"` // AttributeValue{Type, Label} | Icon{URL, URL2x} +} + +// UnmarshalJSON deserializes a JSON-serialized GlanceStatus +func (gs *GlanceStatus) UnmarshalJSON(data []byte) error { + // Compact the JSON to make it easier to process below + buffer := bytes.NewBuffer([]byte{}) + err := json.Compact(buffer, data) + if err != nil { + return err + } + data = buffer.Bytes() + + // Since Value can be either an AttributeValue or an Icon, we + // must check and deserialize appropriately + obj := make(map[string]interface{}) + + err = json.Unmarshal(data, &obj) + if err != nil { + return err + } + + for _, field := range []string{"type", "value"} { + if obj[field] == nil { + return fmt.Errorf("missing %s field", field) + } + } + + gs.Type = obj["type"].(string) + val := obj["value"].(map[string]interface{}) + + valueMap := map[string][]string{ + "lozenge": []string{"type", "label"}, + "icon": []string{"url", "url@2x"}, + } + + if valueMap[gs.Type] == nil { + return fmt.Errorf("invalid GlanceStatus type: %s", gs.Type) + } + + for _, field := range valueMap[gs.Type] { + if val[field] == nil { + return fmt.Errorf("%s missing %s field", gs.Type, field) + } + _, ok := val[field].(string) + if !ok { + return fmt.Errorf("could not convert %s field %s to string", gs.Type, field) + } + } + + // Can safely perform type coercion + switch gs.Type { + case "lozenge": + gs.Value = AttributeValue{Type: val["type"].(string), Label: val["label"].(string)} + case "icon": + gs.Value = Icon{URL: val["url"].(string), URL2x: val["url@2x"].(string)} + } + + return nil +} + +// AddAttribute adds an attribute to a Card +func (c *Card) AddAttribute(mainLabel, subLabel, url, iconURL string) { + attr := Attribute{Label: mainLabel} + attr.Value = AttributeValue{Label: subLabel, URL: url, Icon: &Icon{URL: iconURL}} + + c.Attributes = append(c.Attributes, attr) +} + +// RoomsListOptions specifies the optional parameters of the RoomService.List +// method. +type RoomsListOptions struct { + ListOptions + ExpandOptions + + // Include private rooms in the result, API defaults to true + IncludePrivate bool `url:"include-private,omitempty"` + + // Include archived rooms in the result, API defaults to false + IncludeArchived bool `url:"include-archived,omitempty"` +} + +// List returns all the rooms authorized. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/get_all_rooms +func (r *RoomService) List(opt *RoomsListOptions) (*Rooms, *http.Response, error) { + req, err := r.client.NewRequest("GET", "room", opt, nil) + if err != nil { + return nil, nil, err + } + + rooms := new(Rooms) + resp, err := r.client.Do(req, rooms) + if err != nil { + return nil, resp, err + } + return rooms, resp, nil +} + +// Get returns the room specified by the id. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/get_room +func (r *RoomService) Get(id string) (*Room, *http.Response, error) { + req, err := r.client.NewRequest("GET", fmt.Sprintf("room/%s", id), nil, nil) + if err != nil { + return nil, nil, err + } + + room := new(Room) + resp, err := r.client.Do(req, room) + if err != nil { + return nil, resp, err + } + return room, resp, nil +} + +// GetStatistics returns the room statistics pecified by the id. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/get_room_statistics +func (r *RoomService) GetStatistics(id string) (*RoomStatistics, *http.Response, error) { + req, err := r.client.NewRequest("GET", fmt.Sprintf("room/%s/statistics", id), nil, nil) + if err != nil { + return nil, nil, err + } + + roomStatistics := new(RoomStatistics) + resp, err := r.client.Do(req, roomStatistics) + if err != nil { + return nil, resp, err + } + return roomStatistics, resp, nil +} + +// Notification sends a notification to the room specified by the id. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/send_room_notification +func (r *RoomService) Notification(id string, notifReq *NotificationRequest) (*http.Response, error) { + req, err := r.client.NewRequest("POST", fmt.Sprintf("room/%s/notification", id), nil, notifReq) + if err != nil { + return nil, err + } + + return r.client.Do(req, nil) +} + +// Message sends a message to the room specified by the id. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/send_message +func (r *RoomService) Message(id string, msgReq *RoomMessageRequest) (*http.Response, error) { + req, err := r.client.NewRequest("POST", fmt.Sprintf("room/%s/message", id), nil, msgReq) + if err != nil { + return nil, err + } + + return r.client.Do(req, nil) +} + +// ShareFile sends a file to the room specified by the id. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/share_file_with_room +func (r *RoomService) ShareFile(id string, shareFileReq *ShareFileRequest) (*http.Response, error) { + req, err := r.client.NewFileUploadRequest("POST", fmt.Sprintf("room/%s/share/file", id), shareFileReq) + if err != nil { + return nil, err + } + + return r.client.Do(req, nil) +} + +// Create creates a new room. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/create_room +func (r *RoomService) Create(roomReq *CreateRoomRequest) (*Room, *http.Response, error) { + req, err := r.client.NewRequest("POST", "room", nil, roomReq) + if err != nil { + return nil, nil, err + } + + room := new(Room) + resp, err := r.client.Do(req, room) + if err != nil { + return nil, resp, err + } + return room, resp, nil +} + +// Delete deletes an existing room. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/delete_room +func (r *RoomService) Delete(id string) (*http.Response, error) { + req, err := r.client.NewRequest("DELETE", fmt.Sprintf("room/%s", id), nil, nil) + if err != nil { + return nil, err + } + + return r.client.Do(req, nil) +} + +// Update updates an existing room. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/update_room +func (r *RoomService) Update(id string, roomReq *UpdateRoomRequest) (*http.Response, error) { + req, err := r.client.NewRequest("PUT", fmt.Sprintf("room/%s", id), nil, roomReq) + if err != nil { + return nil, err + } + + return r.client.Do(req, nil) +} + +// HistoryOptions represents a HipChat room chat history request. +type HistoryOptions struct { + ListOptions + ExpandOptions + + // Either the latest date to fetch history for in ISO-8601 format, or 'recent' to fetch + // the latest 75 messages. Paging isn't supported for 'recent', however they are real-time + // values, whereas date queries may not include the most recent messages. + Date string `url:"date,omitempty"` + + // Your timezone. Must be a supported timezone + Timezone string `url:"timezone,omitempty"` + + // Reverse the output such that the oldest message is first. + // For consistent paging, set to 'false'. + Reverse bool `url:"reverse,omitempty"` + + // Either the earliest date to fetch history for the ISO-8601 format string, + // or leave blank to disable this filter. + // to be effective, the API call requires Date also be filled in with an ISO-8601 format string. + EndDate string `url:"end-date,omitempty"` + + // Include records about deleted messages into results (body of a message isn't returned). Set to 'true'. + IncludeDeleted bool `url:"include_deleted,omitempty"` +} + +// History fetches a room's chat history. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/view_room_history +func (r *RoomService) History(id string, opt *HistoryOptions) (*History, *http.Response, error) { + u := fmt.Sprintf("room/%s/history", id) + req, err := r.client.NewRequest("GET", u, opt, nil) + h := new(History) + resp, err := r.client.Do(req, &h) + if err != nil { + return nil, resp, err + } + return h, resp, nil +} + +// LatestHistoryOptions represents a HipChat room chat latest history request. +type LatestHistoryOptions struct { + + // The maximum number of messages to return. + MaxResults int `url:"max-results,omitempty"` + + // Your timezone. Must be a supported timezone. + Timezone string `url:"timezone,omitempty"` + + // The id of the message that is oldest in the set of messages to be returned. + // The server will not return any messages that chronologically precede this message. + NotBefore string `url:"not-before,omitempty"` +} + +// Latest fetches a room's chat history. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/view_recent_room_history +func (r *RoomService) Latest(id string, opt *LatestHistoryOptions) (*History, *http.Response, error) { + u := fmt.Sprintf("room/%s/history/latest", id) + req, err := r.client.NewRequest("GET", u, opt, nil) + h := new(History) + resp, err := r.client.Do(req, &h) + if err != nil { + return nil, resp, err + } + return h, resp, nil +} + +// SetTopic sets Room topic. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/set_topic +func (r *RoomService) SetTopic(id string, topic string) (*http.Response, error) { + topicReq := &SetTopicRequest{Topic: topic} + + req, err := r.client.NewRequest("PUT", fmt.Sprintf("room/%s/topic", id), nil, topicReq) + if err != nil { + return nil, err + } + + return r.client.Do(req, nil) +} + +// Invite someone to the Room. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/invite_user +func (r *RoomService) Invite(room string, user string, reason string) (*http.Response, error) { + reasonReq := &InviteRequest{Reason: reason} + + req, err := r.client.NewRequest("POST", fmt.Sprintf("room/%s/invite/%s", room, user), nil, reasonReq) + if err != nil { + return nil, err + } + + return r.client.Do(req, nil) +} + +// CreateGlance creates a glance in the room specified by the id. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/create_room_glance +func (r *RoomService) CreateGlance(id string, glanceReq *GlanceRequest) (*http.Response, error) { + req, err := r.client.NewRequest("PUT", fmt.Sprintf("room/%s/extension/glance/%s", id, glanceReq.Key), nil, glanceReq) + if err != nil { + return nil, err + } + + return r.client.Do(req, nil) +} + +// DeleteGlance deletes a glance in the room specified by the id. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/delete_room_glance +func (r *RoomService) DeleteGlance(id string, glanceReq *GlanceRequest) (*http.Response, error) { + req, err := r.client.NewRequest("DELETE", fmt.Sprintf("room/%s/extension/glance/%s", id, glanceReq.Key), nil, nil) + if err != nil { + return nil, err + } + + return r.client.Do(req, nil) +} + +// UpdateGlance sends a glance update to the room specified by the id. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/room_addon_ui_update +func (r *RoomService) UpdateGlance(id string, glanceUpdateReq *GlanceUpdateRequest) (*http.Response, error) { + req, err := r.client.NewRequest("POST", fmt.Sprintf("addon/ui/room/%s", id), nil, glanceUpdateReq) + if err != nil { + return nil, err + } + + return r.client.Do(req, nil) +} + +// AddMember adds a member to a private room and sends member's unavailable presence to all room members asynchronously. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/add_member +func (r *RoomService) AddMember(roomID string, userID string, addMemberReq *AddMemberRequest) (*http.Response, error) { + req, err := r.client.NewRequest("PUT", fmt.Sprintf("room/%s/member/%s", roomID, userID), nil, addMemberReq) + if err != nil { + return nil, err + } + + return r.client.Do(req, nil) +} + +// RemoveMember removes a member from a private room +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/remove_member +func (r *RoomService) RemoveMember(roomID string, userID string) (*http.Response, error) { + req, err := r.client.NewRequest("DELETE", fmt.Sprintf("room/%s/member/%s", roomID, userID), nil, nil) + if err != nil { + return nil, err + } + + return r.client.Do(req, nil) +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_test.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_test.go new file mode 100644 index 00000000..59bcf1ef --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_test.go @@ -0,0 +1,734 @@ +package hipchat + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "reflect" + "testing" +) + +func TestRoomGet(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/room/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprintf(w, ` + { + "id":1, + "name":"n", + "links":{"self":"s"}, + "Participants":[ + {"Name":"n1"}, + {"Name":"n2"} + ], + "Owner":{"Name":"n1"} + }`) + }) + want := &Room{ + ID: 1, + Name: "n", + Links: RoomLinks{Links: Links{Self: "s"}}, + Participants: []User{{Name: "n1"}, {Name: "n2"}}, + Owner: User{Name: "n1"}, + } + + room, _, err := client.Room.Get("1") + if err != nil { + t.Fatalf("Room.Get returns an error %v", err) + } + if !reflect.DeepEqual(want, room) { + t.Errorf("Room.Get returned %+v, want %+v", room, want) + } +} + +func TestRoomList(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/room", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{ + "start-index": "1", + "max-results": "10", + "expand": "expansion", + "include-private": "true", + "include-archived": "true", + }) + fmt.Fprintf(w, ` + { + "items": [{"id":1,"name":"n"}], + "startIndex":1, + "maxResults":1, + "links":{"Self":"s"} + }`) + }) + want := &Rooms{Items: []Room{{ID: 1, Name: "n"}}, StartIndex: 1, MaxResults: 1, Links: PageLinks{Links: Links{Self: "s"}}} + opt := &RoomsListOptions{ListOptions{1, 10}, ExpandOptions{"expansion"}, true, true} + rooms, _, err := client.Room.List(opt) + if err != nil { + t.Fatalf("Room.List returns an error %v", err) + } + if !reflect.DeepEqual(want, rooms) { + t.Errorf("Room.List returned %+v, want %+v", rooms, want) + } +} + +func TestRoomGetStatistics(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/room/1/statistics", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprintf(w, ` + { + "messages_sent":1, + "last_active":"2016-02-29T09:03:53+00:00" + }`) + }) + want := &RoomStatistics{ + MessagesSent: 1, + LastActive: "2016-02-29T09:03:53+00:00", + } + + roomStatistics, _, err := client.Room.GetStatistics("1") + if err != nil { + t.Fatalf("Room.GetStatistics returns an error %v", err) + } + if !reflect.DeepEqual(want, roomStatistics) { + t.Errorf("Room.GetStatistics returned %+v, want %+v", roomStatistics, want) + } +} + +func TestRoomNotification(t *testing.T) { + setup() + defer teardown() + + args := &NotificationRequest{Color: "red", Message: "m", MessageFormat: "text"} + + mux.HandleFunc("/room/1/notification", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + v := new(NotificationRequest) + json.NewDecoder(r.Body).Decode(v) + + if !reflect.DeepEqual(v, args) { + t.Errorf("Request body %+v, want %+v", v, args) + } + w.WriteHeader(http.StatusNoContent) + }) + + _, err := client.Room.Notification("1", args) + if err != nil { + t.Fatalf("Room.Notification returns an error %v", err) + } +} + +func TestRoomNotificationCardWithThumbnail(t *testing.T) { + setup() + defer teardown() + + thumbnail := &Thumbnail{URL: "http://foo.com", URL2x: "http://foo2x.com", Width: 1, Height: 2} + description := CardDescription{Format: "format", Value: "value"} + card := &Card{Style: "style", Description: description, Title: "title", Thumbnail: thumbnail} + args := &NotificationRequest{Card: card} + + mux.HandleFunc("/room/2/notification", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + v := new(NotificationRequest) + json.NewDecoder(r.Body).Decode(v) + + if !reflect.DeepEqual(v, args) { + t.Errorf("Request body %+v, want %+v", v, args) + } + w.WriteHeader(http.StatusNoContent) + }) + + _, err := client.Room.Notification("2", args) + if err != nil { + t.Fatalf("Room.Notification returns an error %v", err) + } +} + +func TestRoomMessage(t *testing.T) { + setup() + defer teardown() + + args := &RoomMessageRequest{Message: "m"} + + mux.HandleFunc("/room/1/message", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + v := new(RoomMessageRequest) + json.NewDecoder(r.Body).Decode(v) + + if !reflect.DeepEqual(v, args) { + t.Errorf("Request body %+v, want %+v", v, args) + } + w.WriteHeader(http.StatusNoContent) + }) + + _, err := client.Room.Message("1", args) + if err != nil { + t.Fatalf("Room.Message returns an error %v", err) + } +} + +func TestRoomShareFile(t *testing.T) { + setup() + defer teardown() + + tempFile, err := ioutil.TempFile(os.TempDir(), "hipfile") + tempFile.WriteString("go gophers") + defer os.Remove(tempFile.Name()) + + want := "--hipfileboundary\n" + + "Content-Type: application/json; charset=UTF-8\n" + + "Content-Disposition: attachment; name=\"metadata\"\n\n" + + "{\"message\": \"Hello there\"}\n" + + "--hipfileboundary\n" + + "Content-Type: charset=UTF-8\n" + + "Content-Transfer-Encoding: base64\n" + + "Content-Disposition: attachment; name=file; filename=hipfile\n\n" + + "Z28gZ29waGVycw==\n" + + "--hipfileboundary\n" + + mux.HandleFunc("/room/1/share/file", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + + body, _ := ioutil.ReadAll(r.Body) + + if string(body) != want { + t.Errorf("Request body \n%+v\n,want \n\n%+v", string(body), want) + } + w.WriteHeader(http.StatusNoContent) + }) + + args := &ShareFileRequest{Path: tempFile.Name(), Message: "Hello there", Filename: "hipfile"} + _, err = client.Room.ShareFile("1", args) + if err != nil { + t.Fatalf("Room.ShareFile returns an error %v", err) + } +} + +func TestRoomCreate(t *testing.T) { + setup() + defer teardown() + + args := &CreateRoomRequest{Name: "n", Topic: "t"} + + mux.HandleFunc("/room", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + v := new(CreateRoomRequest) + json.NewDecoder(r.Body).Decode(v) + + if !reflect.DeepEqual(v, args) { + t.Errorf("Request body %+v, want %+v", v, args) + } + fmt.Fprintf(w, `{"id":1,"links":{"self":"s"}}`) + }) + want := &Room{ID: 1, Links: RoomLinks{Links: Links{Self: "s"}}} + + room, _, err := client.Room.Create(args) + if err != nil { + t.Fatalf("Room.Create returns an error %v", err) + } + if !reflect.DeepEqual(room, want) { + t.Errorf("Room.Create returns %+v, want %+v", room, want) + } +} + +func TestRoomDelete(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/room/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + _, err := client.Room.Delete("1") + if err != nil { + t.Fatalf("Room.Delete returns an error %v", err) + } +} + +func TestRoomUpdate(t *testing.T) { + setup() + defer teardown() + + args := &UpdateRoomRequest{Name: "n", Topic: "t"} + + mux.HandleFunc("/room/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + v := new(UpdateRoomRequest) + json.NewDecoder(r.Body).Decode(v) + + if !reflect.DeepEqual(v, args) { + t.Errorf("Request body %+v, want %+v", v, args) + } + }) + + _, err := client.Room.Update("1", args) + if err != nil { + t.Fatalf("Room.Update returns an error %v", err) + } +} + +func TestRoomHistory(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/room/1/history", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{ + "start-index": "1", + "max-results": "100", + "expand": "expansion", + "date": "date", + "timezone": "tz", + "reverse": "true", + "end-date": "end-date", + "include_deleted": "true", + }) + fmt.Fprintf(w, ` + { + "items": [ + { + "date": "2014-11-23T21:23:49.807578+00:00", + "from": "Test Testerson", + "id": "f058e668-c9c0-4cd5-9ca5-e2c42b06f3ed", + "mentions": [], + "message": "Hey there!", + "message_format": "html", + "type": "notification" + } + ], + "links": { + "self": "https://api.hipchat.com/v2/room/1/history" + }, + "maxResults": 100, + "startIndex": 0 + }`) + }) + + opt := &HistoryOptions{ + ListOptions{1, 100}, ExpandOptions{"expansion"}, "date", "tz", true, "end-date", true, + } + hist, _, err := client.Room.History("1", opt) + if err != nil { + t.Fatalf("Room.History returns an error %v", err) + } + + want := &History{Items: []Message{{Date: "2014-11-23T21:23:49.807578+00:00", From: "Test Testerson", ID: "f058e668-c9c0-4cd5-9ca5-e2c42b06f3ed", Mentions: []User{}, Message: "Hey there!", MessageFormat: "html", Type: "notification"}}, StartIndex: 0, MaxResults: 100, Links: PageLinks{Links: Links{Self: "https://api.hipchat.com/v2/room/1/history"}}} + if !reflect.DeepEqual(want, hist) { + t.Errorf("Room.History returned %+v, want %+v", hist, want) + } +} + +func TestRoomLatest(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/room/1/history/latest", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{ + "max-results": "100", + "timezone": "tz", + "not-before": "notbefore", + }) + fmt.Fprintf(w, ` + { + "items": [ + { + "date": "2014-11-23T21:23:49.807578+00:00", + "from": "Test Testerson", + "id": "f058e668-c9c0-4cd5-9ca5-e2c42b06f3ed", + "mentions": [], + "message": "Hey there!", + "message_format": "html", + "type": "notification" + } + ], + "links": { + "self": "https://api.hipchat.com/v2/room/1/history/latest" + }, + "maxResults": 100 + }`) + }) + + opt := &LatestHistoryOptions{ + 100, "tz", "notbefore", + } + hist, _, err := client.Room.Latest("1", opt) + if err != nil { + t.Fatalf("Room.Latest returns an error %v", err) + } + want := &History{Items: []Message{{Date: "2014-11-23T21:23:49.807578+00:00", From: "Test Testerson", ID: "f058e668-c9c0-4cd5-9ca5-e2c42b06f3ed", Mentions: []User{}, Message: "Hey there!", MessageFormat: "html", Type: "notification"}}, MaxResults: 100, Links: PageLinks{Links: Links{Self: "https://api.hipchat.com/v2/room/1/history/latest"}}} + if !reflect.DeepEqual(want, hist) { + t.Errorf("Room.Latest returned %+v, want %+v", hist, want) + } +} +func TestRoomGlanceCreate(t *testing.T) { + setup() + defer teardown() + + args := &GlanceRequest{ + Key: "abc", + Name: GlanceName{Value: "Test Glance"}, + Target: "target", + QueryURL: "qu", + Icon: Icon{URL: "i", URL2x: "i"}, + } + + mux.HandleFunc(fmt.Sprintf("/room/1/extension/glance/%s", args.Key), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + v := new(GlanceRequest) + json.NewDecoder(r.Body).Decode(v) + if !reflect.DeepEqual(v, args) { + t.Errorf("Request body %+v, want %+v", v, args) + } + w.WriteHeader(http.StatusNoContent) + }) + + _, err := client.Room.CreateGlance("1", args) + if err != nil { + t.Fatalf("Room.CreateGlance returns an error %v", err) + } +} + +func TestRoomGlanceDelete(t *testing.T) { + setup() + defer teardown() + + args := &GlanceRequest{ + Key: "abc", + } + + mux.HandleFunc(fmt.Sprintf("/room/1/extension/glance/%s", args.Key), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + _, err := client.Room.DeleteGlance("1", args) + if err != nil { + t.Fatalf("Room.DeleteGlance returns an error %v", err) + } +} + +func TestRoomGlanceUpdate(t *testing.T) { + setup() + defer teardown() + + args := &GlanceUpdateRequest{ + Glance: []*GlanceUpdate{ + &GlanceUpdate{ + Key: "abc", + Content: GlanceContent{ + Status: &GlanceStatus{Type: "lozenge", Value: AttributeValue{Type: "default", Label: "something"}}, + Label: AttributeValue{Type: "html", Value: "hello"}, + }, + }, + }, + } + + mux.HandleFunc("/addon/ui/room/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + v := new(GlanceUpdateRequest) + json.NewDecoder(r.Body).Decode(v) + if !reflect.DeepEqual(v, args) { + t.Errorf("Request body %+v, want %+v", v, args) + } + w.WriteHeader(http.StatusNoContent) + }) + + _, err := client.Room.UpdateGlance("1", args) + if err != nil { + t.Fatalf("Room.UpdateGlance returns an error %v", err) + } +} + +func TestSetTopic(t *testing.T) { + setup() + defer teardown() + + args := &SetTopicRequest{Topic: "t"} + + mux.HandleFunc("/room/1/topic", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + v := new(SetTopicRequest) + json.NewDecoder(r.Body).Decode(v) + + if !reflect.DeepEqual(v, args) { + t.Errorf("Request body %+v, want %+v", v, args) + } + }) + + _, err := client.Room.SetTopic("1", "t") + if err != nil { + t.Fatalf("Room.SetTopic returns an error %v", err) + } +} + +func TestInvite(t *testing.T) { + setup() + defer teardown() + + args := &InviteRequest{Reason: "r"} + + mux.HandleFunc("/room/1/invite/user", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + v := new(InviteRequest) + json.NewDecoder(r.Body).Decode(v) + + if !reflect.DeepEqual(v, args) { + t.Errorf("Request body %+v, want %+v", v, args) + } + }) + + _, err := client.Room.Invite("1", "user", "r") + if err != nil { + t.Fatalf("Room.Invite returns an error %v", err) + } +} + +func TestCardDescriptionJSONEncodeWithString(t *testing.T) { + description := CardDescription{Value: "This is a test"} + expected := `"This is a test"` + + encoded, err := json.Marshal(description) + if err != nil { + t.Errorf("Encoding of CardDescription failed") + } + + if string(encoded) != expected { + t.Fatalf("Encoding of CardDescription failed: %s", encoded) + } +} + +func TestCardDescriptionJSONDecodeWithString(t *testing.T) { + encoded := []byte(`"This is a test"`) + expected := CardDescription{Format: "", Value: "This is a test"} + + var actual CardDescription + + err := json.Unmarshal(encoded, &actual) + if err != nil { + t.Errorf("Decoding of CardDescription failed: %v", err) + } + + if actual.Value != expected.Value { + t.Fatalf("Unexpected CardDescription.Value: %v", actual.Value) + } + + if actual.Format != expected.Format { + t.Fatalf("Unexpected CardDescription.Format: %v", actual.Format) + } +} + +func TestCardDescriptionJSONEncodeWithObject(t *testing.T) { + description := CardDescription{Format: "html", Value: "This is a test"} + expected := `{"format":"html","value":"\u003cstrong\u003eThis is a test\u003c/strong\u003e"}` + + encoded, err := json.Marshal(description) + if err != nil { + t.Errorf("Encoding of CardDescription failed") + } + + if string(encoded) != expected { + t.Fatalf("Encoding of CardDescription failed: %s", encoded) + } +} + +func TestCardDescriptionJSONDecodeWithObject(t *testing.T) { + encoded := []byte(`{"format":"html","value":"\u003cstrong\u003eThis is a test\u003c/strong\u003e"}`) + expected := CardDescription{Format: "html", Value: "This is a test"} + + var actual CardDescription + + err := json.Unmarshal(encoded, &actual) + if err != nil { + t.Errorf("Decoding of CardDescription failed: %v", err) + } + + if actual.Value != expected.Value { + t.Fatalf("Unexpected CardDescription.Value: %v", actual.Value) + } + + if actual.Format != expected.Format { + t.Fatalf("Unexpected CardDescription.Format: %v", actual.Format) + } +} + +func TestGlanceUpdateRequestJSONEncodeWithString(t *testing.T) { + gr := GlanceUpdateRequest{ + Glance: []*GlanceUpdate{ + &GlanceUpdate{ + Key: "abc", + Content: GlanceContent{ + Status: &GlanceStatus{Type: "lozenge", Value: AttributeValue{Type: "default", Label: "something"}}, + Label: AttributeValue{Type: "html", Value: "hello"}, + }, + }, + }, + } + expected := `{"glance":[{"key":"abc","content":{"status":{"type":"lozenge","value":{"type":"default","label":"something"}},"label":{"type":"html","value":"hello"}}}]}` + + encoded, err := json.Marshal(gr) + if err != nil { + t.Errorf("Encoding of GlanceUpdateRequest failed") + } + + if string(encoded) != expected { + t.Fatalf("Encoding of GlanceUpdateRequest failed: %s", encoded) + } +} + +func TestGlanceContentJSONEncodeWithString(t *testing.T) { + gcTests := []struct { + gc GlanceContent + expected string + }{ + { + GlanceContent{ + Status: &GlanceStatus{Type: "lozenge", Value: AttributeValue{Type: "default", Label: "something"}}, + Label: AttributeValue{Type: "html", Value: "hello"}, + }, + `{"status":{"type":"lozenge","value":{"type":"default","label":"something"}},"label":{"type":"html","value":"hello"}}`, + }, + } + + for _, tt := range gcTests { + encoded, err := json.Marshal(tt.gc) + if err != nil { + t.Errorf("Encoding of GlanceContent failed") + } + + if string(encoded) != tt.expected { + t.Fatalf("Encoding of GlanceContent failed: %s", encoded) + } + } +} + +func TestGlanceContentJSONDecodeWithObject(t *testing.T) { + gcTests := []struct { + gc GlanceContent + encoded string + }{ + { + GlanceContent{ + Status: &GlanceStatus{Type: "lozenge", Value: AttributeValue{Type: "default", Label: "something"}}, + Label: AttributeValue{Type: "html", Value: "hello"}, + }, + `{"status":{"type":"lozenge","value":{"type":"default","label":"something"}},"label":{"type":"html","value":"hello"}}`, + }, + } + + for _, tt := range gcTests { + var actual GlanceContent + + err := json.Unmarshal([]byte(tt.encoded), &actual) + if err != nil { + t.Errorf("Decoding of GlanceContent failed: %v", err) + } + + if !reflect.DeepEqual(actual.Status, tt.gc.Status) { + t.Fatalf("Unexpected GlanceContent.Status: %+v, want %+v", actual.Status, tt.gc.Status) + } + + if actual.Label != tt.gc.Label { + t.Fatalf("Unexpected GlanceStatus.Label: %v", actual.Label) + } + + if actual.Metadata != tt.gc.Metadata { + t.Fatalf("Unexpected GlanceStatus.Metadata %v", actual.Metadata) + } + } +} + +func TestGlanceStatusJSONEncodeWithString(t *testing.T) { + gsTests := []struct { + gs *GlanceStatus + expected string + }{ + {&GlanceStatus{Type: "lozenge", Value: AttributeValue{Type: "default", Label: "something"}}, + `{"type":"lozenge","value":{"type":"default","label":"something"}}`}, + {&GlanceStatus{Type: "icon", Value: Icon{URL: "z", URL2x: "x"}}, + `{"type":"icon","value":{"url":"z","url@2x":"x"}}`}, + } + + for _, tt := range gsTests { + encoded, err := json.Marshal(tt.gs) + if err != nil { + t.Errorf("Encoding of GlanceStatus failed") + } + + if string(encoded) != tt.expected { + t.Fatalf("Encoding of GlanceStatus failed: %s", encoded) + } + } +} + +func TestGlanceStatusJSONDecodeWithObject(t *testing.T) { + gsTests := []struct { + gs *GlanceStatus + encoded string + }{ + {&GlanceStatus{Type: "lozenge", Value: AttributeValue{Type: "default", Label: "something"}}, + `{"type":"lozenge","value":{"type":"default","label":"something"}}`}, + {&GlanceStatus{Type: "icon", Value: Icon{URL: "z", URL2x: "x"}}, + `{"type":"icon","value":{"url":"z","url@2x":"x"}}`}, + } + + for _, tt := range gsTests { + var actual GlanceStatus + + err := json.Unmarshal([]byte(tt.encoded), &actual) + if err != nil { + t.Errorf("Decoding of GlanceStatus failed: %v", err) + } + + if actual.Type != tt.gs.Type { + t.Fatalf("Unexpected GlanceStatus.Type: %v", actual.Type) + } + + if actual.Value != tt.gs.Value { + t.Fatalf("Unexpected GlanceStatus.Value: %v", actual.Value) + } + } +} + +func TestAddMember(t *testing.T) { + setup() + defer teardown() + + args := &AddMemberRequest{Roles: []string{"room_member"}} + + mux.HandleFunc("/room/1/member/user", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + v := new(AddMemberRequest) + json.NewDecoder(r.Body).Decode(v) + + if !reflect.DeepEqual(v, args) { + t.Errorf("Request body %+v, want %+v", v, args) + } + }) + + _, err := client.Room.AddMember("1", "user", args) + if err != nil { + t.Fatalf("Room.AddMember returns an error %v", err) + } +} + +func TestRemoveMember(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/room/1/member/user", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + _, err := client.Room.RemoveMember("1", "user") + if err != nil { + t.Fatalf("Room.RemoveMember returns an error %v", err) + } +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_webhook.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_webhook.go new file mode 100644 index 00000000..233296d6 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_webhook.go @@ -0,0 +1,99 @@ +// handling Webhook data + +package hipchat + +import ( + "fmt" + "net/http" +) + +// Response Types + +// Webhook represents a HipChat webhook. +type Webhook struct { + Links Links `json:"links"` + Name string `json:"name"` + Key string `json:"key,omitempty"` + Event string `json:"event"` + Pattern string `json:"pattern"` + URL string `json:"url"` + ID int `json:"id,omitempty"` +} + +// WebhookList represents a HipChat webhook list. +type WebhookList struct { + Webhooks []Webhook `json:"items"` + StartIndex int `json:"startIndex"` + MaxResults int `json:"maxResults"` + Links PageLinks `json:"links"` +} + +// Request Types + +// ListWebhooksOptions represents options for ListWebhooks method. +type ListWebhooksOptions struct { + ListOptions +} + +// CreateWebhookRequest represents the body of the CreateWebhook method. +type CreateWebhookRequest struct { + Name string `json:"name"` + Key string `json:"key,omitempty"` + Event string `json:"event"` + Pattern string `json:"pattern"` + URL string `json:"url"` +} + +// ListWebhooks returns all the webhooks for a given room. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/get_all_webhooks +func (r *RoomService) ListWebhooks(id interface{}, opt *ListWebhooksOptions) (*WebhookList, *http.Response, error) { + u := fmt.Sprintf("room/%v/webhook", id) + req, err := r.client.NewRequest("GET", u, opt, nil) + if err != nil { + return nil, nil, err + } + whList := new(WebhookList) + + resp, err := r.client.Do(req, whList) + if err != nil { + return nil, resp, err + } + return whList, resp, nil +} + +// DeleteWebhook removes the given webhook. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/delete_webhook +func (r *RoomService) DeleteWebhook(id interface{}, webhookID interface{}) (*http.Response, error) { + req, err := r.client.NewRequest("DELETE", fmt.Sprintf("room/%v/webhook/%v", id, webhookID), nil, nil) + if err != nil { + return nil, err + } + + resp, err := r.client.Do(req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} + +// CreateWebhook creates a new webhook. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/create_webhook +func (r *RoomService) CreateWebhook(id interface{}, roomReq *CreateWebhookRequest) (*Webhook, *http.Response, error) { + req, err := r.client.NewRequest("POST", fmt.Sprintf("room/%v/webhook", id), nil, roomReq) + if err != nil { + return nil, nil, err + } + + wh := new(Webhook) + + resp, err := r.client.Do(req, wh) + if err != nil { + return nil, resp, err + } + + return wh, resp, nil +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_webhook_test.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_webhook_test.go new file mode 100644 index 00000000..4ec5ad5d --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_webhook_test.go @@ -0,0 +1,82 @@ +package hipchat + +import ( + // "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestWebhookList(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/room/1/webhook", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{ + "max-results": "100", + "start-index": "1", + }) + fmt.Fprintf(w, ` + { + "items":[ + {"name":"a", "key": "a", "pattern":"a", "event":"message_received", "url":"h", "id":1, "links":{"self":"s"}}, + {"name":"b", "key": "b", "pattern":"b", "event":"message_received", "url":"h", "id":2, "links":{"self":"s"}} + ], + "links":{"self":"s", "prev":"a", "next":"b"}, + "startIndex":0, + "maxResults":10 + }`) + }) + + want := &WebhookList{ + Webhooks: []Webhook{ + { + Name: "a", + Key: "a", + Pattern: "a", + Event: "message_received", + URL: "h", + ID: 1, + Links: Links{Self: "s"}, + }, + { + Name: "b", + Key: "b", + Pattern: "b", + Event: "message_received", + URL: "h", + ID: 2, + Links: Links{Self: "s"}, + }, + }, + StartIndex: 0, + MaxResults: 10, + Links: PageLinks{Links: Links{Self: "s"}, Prev: "a", Next: "b"}, + } + + opt := &ListWebhooksOptions{ListOptions{1, 100}} + + actual, _, err := client.Room.ListWebhooks("1", opt) + if err != nil { + t.Fatalf("Room.ListWebhooks returns an error %v", err) + } + if !reflect.DeepEqual(want, actual) { + t.Errorf("Room.ListWebhooks returned %+v, want %+v", actual, want) + } +} + +func TestWebhookDelete(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/room/1/webhook/2", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + _, err := client.Room.DeleteWebhook("1", "2") + if err != nil { + t.Fatalf("Room.Update returns an error %v", err) + } +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/user.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/user.go new file mode 100644 index 00000000..c45872ec --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/user.go @@ -0,0 +1,155 @@ +package hipchat + +import ( + "fmt" + "net/http" +) + +// MessageRequest represents a HipChat private message to user. +type MessageRequest struct { + Message string `json:"message,omitempty"` + Notify bool `json:"notify,omitempty"` + MessageFormat string `json:"message_format,omitempty"` +} + +// UserPresence represents the HipChat user's presence. +type UserPresence struct { + Status string `json:"status"` + Idle int `json:"idle"` + Show string `json:"show"` + IsOnline bool `json:"is_online"` +} + +const ( + // UserPresenceShowAway show status away + UserPresenceShowAway = "away" + + // UserPresenceShowChat show status available to chat + UserPresenceShowChat = "chat" + + // UserPresenceShowDnd show status do not disturb + UserPresenceShowDnd = "dnd" + + // UserPresenceShowXa show status xa? + UserPresenceShowXa = "xa" +) + +// UpdateUserRequest represents a HipChat user update request body. +type UpdateUserRequest struct { + Name string `json:"name"` + Presence UpdateUserPresenceRequest `json:"presence"` + MentionName string `json:"mention_name"` + Email string `json:"email"` +} + +// UpdateUserPresenceRequest represents the HipChat user's presence update request body. +type UpdateUserPresenceRequest struct { + Status string `json:"status"` + Show string `json:"show"` +} + +// User represents the HipChat user. +type User struct { + XMPPJid string `json:"xmpp_jid"` + IsDeleted bool `json:"is_deleted"` + Name string `json:"name"` + LastActive string `json:"last_active"` + Title string `json:"title"` + Presence UserPresence `json:"presence"` + Created string `json:"created"` + ID int `json:"id"` + MentionName string `json:"mention_name"` + IsGroupAdmin bool `json:"is_group_admin"` + Timezone string `json:"timezone"` + IsGuest bool `json:"is_guest"` + Email string `json:"email"` + PhotoURL string `json:"photo_url"` + Links Links `json:"links"` +} + +// Users represents the API return of a collection of Users plus metadata +type Users struct { + Items []User `json:"items"` + StartIndex int `json:"start_index"` + MaxResults int `json:"max_results"` + Links Links `json:"links"` +} + +// UserService gives access to the user related methods of the API. +type UserService struct { + client *Client +} + +// ShareFile sends a file to the user specified by the id. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/share_file_with_user +func (u *UserService) ShareFile(id string, shareFileReq *ShareFileRequest) (*http.Response, error) { + req, err := u.client.NewFileUploadRequest("POST", fmt.Sprintf("user/%s/share/file", id), shareFileReq) + if err != nil { + return nil, err + } + + return u.client.Do(req, nil) +} + +// View fetches a user's details. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/view_user +func (u *UserService) View(id string) (*User, *http.Response, error) { + req, err := u.client.NewRequest("GET", fmt.Sprintf("user/%s", id), nil, nil) + + userDetails := new(User) + resp, err := u.client.Do(req, &userDetails) + if err != nil { + return nil, resp, err + } + return userDetails, resp, nil +} + +// Message sends a private message to the user specified by the id. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/private_message_user +func (u *UserService) Message(id string, msgReq *MessageRequest) (*http.Response, error) { + req, err := u.client.NewRequest("POST", fmt.Sprintf("user/%s/message", id), nil, msgReq) + if err != nil { + return nil, err + } + + return u.client.Do(req, nil) +} + +// UserListOptions specified the parameters to the UserService.List method. +type UserListOptions struct { + ListOptions + ExpandOptions + // Include active guest users in response. + IncludeGuests bool `url:"include-guests,omitempty"` + // Include deleted users in response. + IncludeDeleted bool `url:"include-deleted,omitempty"` +} + +// List returns all users in the group. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/get_all_users +func (u *UserService) List(opt *UserListOptions) ([]User, *http.Response, error) { + req, err := u.client.NewRequest("GET", "user", opt, nil) + + users := new(Users) + resp, err := u.client.Do(req, &users) + if err != nil { + return nil, resp, err + } + return users.Items, resp, nil +} + +// Update a user +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/update_user +func (u *UserService) Update(id string, user *UpdateUserRequest) (*http.Response, error) { + req, err := u.client.NewRequest("PUT", fmt.Sprintf("user/%s", id), nil, user) + if err != nil { + return nil, err + } + + return u.client.Do(req, nil) +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/user_test.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/user_test.go new file mode 100644 index 00000000..9e245673 --- /dev/null +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/user_test.go @@ -0,0 +1,216 @@ +package hipchat + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "reflect" + "testing" +) + +func TestUserShareFile(t *testing.T) { + setup() + defer teardown() + + tempFile, err := ioutil.TempFile(os.TempDir(), "hipfile") + tempFile.WriteString("go gophers") + defer os.Remove(tempFile.Name()) + + want := "--hipfileboundary\n" + + "Content-Type: application/json; charset=UTF-8\n" + + "Content-Disposition: attachment; name=\"metadata\"\n\n" + + "{\"message\": \"Hello there\"}\n" + + "--hipfileboundary\n" + + "Content-Type: charset=UTF-8\n" + + "Content-Transfer-Encoding: base64\n" + + "Content-Disposition: attachment; name=file; filename=hipfile\n\n" + + "Z28gZ29waGVycw==\n" + + "--hipfileboundary\n" + + mux.HandleFunc("/user/1/share/file", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + + body, _ := ioutil.ReadAll(r.Body) + + if string(body) != want { + t.Errorf("Request body \n%+v\n,want \n\n%+v", string(body), want) + } + w.WriteHeader(http.StatusNoContent) + }) + + args := &ShareFileRequest{Path: tempFile.Name(), Message: "Hello there", Filename: "hipfile"} + _, err = client.User.ShareFile("1", args) + if err != nil { + t.Fatalf("User.ShareFile returns an error %v", err) + } +} + +func TestUserMessage(t *testing.T) { + setup() + defer teardown() + + args := &MessageRequest{Message: "m", MessageFormat: "text"} + + mux.HandleFunc("/user/@FirstL/message", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + v := new(MessageRequest) + json.NewDecoder(r.Body).Decode(v) + + if !reflect.DeepEqual(v, args) { + t.Errorf("Request body %+v, want %+v", v, args) + } + w.WriteHeader(http.StatusNoContent) + }) + + _, err := client.User.Message("@FirstL", args) + if err != nil { + t.Fatalf("User.Message returns an error %v", err) + } +} + +func TestUserView(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/user/@FirstL", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprintf(w, ` + { + "created": "2013-11-07T17:57:11+00:00", + "email": "user@example.com", + "group": { + "id": 1234, + "links": { + "self": "https://api.hipchat.com/v2/group/1234" + }, + "name": "Example" + }, + "id": 1, + "is_deleted": false, + "is_group_admin": true, + "is_guest": false, + "last_active": "1421029691", + "links": { + "self": "https://api.hipchat.com/v2/user/1" + }, + "mention_name": "FirstL", + "name": "First Last", + "photo_url": "https://bitbucket-assetroot.s3.amazonaws.com/c/photos/2014/Mar/02/hipchat-pidgin-theme-logo-571708621-0_avatar.png", + "presence": { + "client": { + "type": "http://hipchat.com/client/mac", + "version": "151" + }, + "is_online": true, + "show": "chat" + }, + "timezone": "America/New_York", + "title": "Test user", + "xmpp_jid": "1@chat.hipchat.com" + }`) + }) + want := &User{XMPPJid: "1@chat.hipchat.com", + IsDeleted: false, + Name: "First Last", + LastActive: "1421029691", + Title: "Test user", + Presence: UserPresence{Show: "chat", IsOnline: true}, + Created: "2013-11-07T17:57:11+00:00", + ID: 1, + MentionName: "FirstL", + IsGroupAdmin: true, + Timezone: "America/New_York", + IsGuest: false, + Email: "user@example.com", + PhotoURL: "https://bitbucket-assetroot.s3.amazonaws.com/c/photos/2014/Mar/02/hipchat-pidgin-theme-logo-571708621-0_avatar.png", + Links: Links{Self: "https://api.hipchat.com/v2/user/1"}} + + user, _, err := client.User.View("@FirstL") + if err != nil { + t.Fatalf("User.View returns an error %v", err) + } + if !reflect.DeepEqual(want, user) { + t.Errorf("User.View returned %+v, want %+v", user, want) + } +} + +func TestUserList(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{ + "start-index": "1", + "max-results": "100", + "expand": "expansion", + "include-guests": "true", + "include-deleted": "true", + }) + fmt.Fprintf(w, ` + { + "items": [ + { + "id": 1, + "links": { + "self": "https:\/\/api.hipchat.com\/v2\/user\/1" + }, + "mention_name": "FirstL", + "name": "First Last" + } + ], + "startIndex": 0, + "maxResults": 100, + "links": { + "self": "https:\/\/api.hipchat.com\/v2\/user" + } + }`) + }) + want := []User{ + { + ID: 1, + Name: "First Last", + MentionName: "FirstL", + Links: Links{Self: "https://api.hipchat.com/v2/user/1"}, + }, + } + + opt := &UserListOptions{ + ListOptions{StartIndex: 1, MaxResults: 100}, + ExpandOptions{"expansion"}, + true, true, + } + + users, _, err := client.User.List(opt) + if err != nil { + t.Fatalf("User.List returned an error %v", err) + } + if !reflect.DeepEqual(want, users) { + t.Errorf("User.List returned %+v, want %+v", users, want) + } +} + +func TestUserUpdate(t *testing.T) { + setup() + defer teardown() + + pArgs := UpdateUserPresenceRequest{Status: "status", Show: UserPresenceShowDnd} + userArgs := &UpdateUserRequest{Name: "n", Presence: pArgs, MentionName: "mn", Email: "e"} + + mux.HandleFunc("/user/@FirstL", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + v := new(UpdateUserRequest) + json.NewDecoder(r.Body).Decode(v) + + if !reflect.DeepEqual(v, userArgs) { + t.Errorf("Request body %+v, want %+v", v, userArgs) + } + }) + + _, err := client.User.Update("@FirstL", userArgs) + if err != nil { + t.Fatalf("User.Update returns an error %v", err) + } +}