Initial TeamSpeak 3 ServerQuery client

Initial version of the TeamSpeak 3 ServerQuery client with function command support for:
Basic: Login, Logout, Version, Use, UsePort
Server: List, IDGetByPort, Info, Create, Edit, Delete, Start, Stop, GroupList, PrivilegeKeyList, PrivilegeKeyAdd

All commands are support via flexible command and argument processing, including automatic decoding of returned data.
This commit is contained in:
Steven Hartland 2017-07-13 00:17:37 +00:00
Родитель 0be7b57c66
Коммит 8881492360
18 изменённых файлов: 1737 добавлений и 2 удалений

2
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,2 @@
*.swp
*.tmp

16
.travis.yml Normal file
Просмотреть файл

@ -0,0 +1,16 @@
language: go
go:
- 1.8.x
- tip
- master
install:
- go get github.com/stretchr/testify/assert
- go get github.com/mitchellh/mapstructure
- go get -u gopkg.in/alecthomas/gometalinter.v1
- gometalinter.v1 --install
script:
- go test -v -race ./...
- gometalinter.v1 --cyclo-over=15 ./...

Просмотреть файл

@ -1,2 +1,54 @@
# go-teamspeak3
A golang TeamSpeak 3 ServerQuery client
# TeamSpeak 3 [![Go Report Card](https://goreportcard.com/badge/github.com/multiplay/go-ts3)](https://goreportcard.com/report/github.com/multiplay/go-ts3) [![License](https://img.shields.io/badge/license-BSD-blue.svg)](https://github.com/multiplay/go-ts3/blob/master/LICENSE) [![GoDoc](https://godoc.org/github.com/multiplay/go-ts3?status.svg)](https://godoc.org/github.com/multiplay/go-ts3) [![Build Status](https://travis-ci.org/multiplay/go-ts3.svg?branch=master)](https://travis-ci.org/multiplay/go-ts3)
go-ts3 is a [Go](http://golang.org/) client for the [TeamSpeak 3 ServerQuery Protocol](http://media.teamspeak.com/ts3_literature/TeamSpeak%203%20Server%20Query%20Manual.pdf).
Features
--------
* [ServerQuery](http://media.teamspeak.com/ts3_literature/TeamSpeak%203%20Server%20Query%20Manual.pdf) Support.
Installation
------------
```sh
go get -u github.com/multiplay/go-ts3
```
Examples
--------
Using go-ts3 is simple just create a client, login and then send commands e.g.
```go
package main
import (
"log"
"github.com/multiplay/go-ts3"
)
func main() {
c, err := ts3.NewClient("192.168.1.102:10011")
if err != nil {
log.Fatal(err)
}
defer c.Close()
if err := c.Login(user, pass); err != nil {
log.Fatal(er)
}
if v, err := c.Version(); err != nil {
log.Fatal(er)
} else {
log.Println("server is running:", v)
}
}
```
Documentation
-------------
- [GoDoc API Reference](http://godoc.org/github.com/multiplay/go-ts3).
License
-------
go-ts3 is available under the [BSD 2-Clause License](https://opensource.org/licenses/BSD-2-Clause).
```

45
basic_cmds.go Normal file
Просмотреть файл

@ -0,0 +1,45 @@
package ts3
// Login authenticates with the server.
func (c *Client) Login(user, passwd string) error {
_, err := c.ExecCmd(NewCmd("login").WithArgs(
NewArg("client_login_name", user),
NewArg("client_login_password", passwd)),
)
return err
}
// Logout deselect virtual server and log out.
func (c *Client) Logout() error {
_, err := c.Exec("logout")
return err
}
// Version represents version information.
type Version struct {
Version string
Platform string
Build int
}
// Version returns version information.
func (c *Client) Version() (*Version, error) {
v := &Version{}
if _, err := c.ExecCmd(NewCmd("version").WithResponse(v)); err != nil {
return nil, err
}
return v, nil
}
// Use selects a virtual server by id.
func (c *Client) Use(id int) error {
_, err := c.ExecCmd(NewCmd("use").WithArgs(NewArg("sid", id)))
return err
}
// UsePort selects a virtual server by port.
func (c *Client) UsePort(port int) error {
_, err := c.ExecCmd(NewCmd("use").WithArgs(NewArg("port", port)))
return err
}

70
basic_cmds_test.go Normal file
Просмотреть файл

@ -0,0 +1,70 @@
package ts3
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCmdsBasic(t *testing.T) {
s := newServer(t)
if s == nil {
return
}
defer func() {
assert.NoError(t, s.Close())
}()
c, err := NewClient(s.Addr, Timeout(time.Second*2))
if !assert.NoError(t, err) {
return
}
defer func() {
assert.NoError(t, c.Close())
}()
auth := func(t *testing.T) {
if err = c.Login("user", "pass"); !assert.NoError(t, err) {
return
}
if err = c.Logout(); !assert.NoError(t, err) {
return
}
}
version := func(t *testing.T) {
v, err := c.Version()
if !assert.NoError(t, err) {
return
}
assert.Equal(t, "3.0.12.2", v.Version)
assert.Equal(t, 1455547898, v.Build)
assert.Equal(t, "FreeBSD", v.Platform)
}
useID := func(t *testing.T) {
assert.NoError(t, c.Use(1))
}
usePort := func(t *testing.T) {
assert.NoError(t, c.UsePort(1024))
}
tests := []struct {
name string
f func(t *testing.T)
}{
{"auth", auth},
{"version", version},
{"useid", useID},
{"userport", usePort},
}
for _, tc := range tests {
t.Run(tc.name, tc.f)
}
}

154
client.go Normal file
Просмотреть файл

@ -0,0 +1,154 @@
package ts3
import (
"bufio"
"fmt"
"io"
"net"
"regexp"
"strings"
"time"
)
const (
// DefaultPort is the default TeamSpeak 3 ServerQuery port.
DefaultPort = 10011
connectHeader = "TS3"
)
var (
respTrailerRe = regexp.MustCompile(`^error id=(\d+) msg=([^ ]+)(.*)`)
// DefaultTimeout is the default read / write / dial timeout for Clients.
DefaultTimeout = time.Second * 10
)
// Client is a TeamSpeak 3 ServerQuery client.
type Client struct {
conn net.Conn
timeout time.Duration
scanner *bufio.Scanner
Server *ServerMethods
}
// Timeout sets read / write / dial timeout for a TeamSpeak 3 Client.
func Timeout(timeout time.Duration) func(*Client) error {
return func(c *Client) error {
c.timeout = timeout
return nil
}
}
// NewClient returns a new TeamSpeak 3 client connected to addr.
func NewClient(addr string, options ...func(c *Client) error) (*Client, error) {
if !strings.Contains(addr, ":") {
addr = fmt.Sprintf("%v:%v", addr, DefaultPort)
}
c := &Client{timeout: DefaultTimeout}
for _, f := range options {
if f == nil {
return nil, ErrNilOption
}
if err := f(c); err != nil {
return nil, err
}
}
// Wire up command groups
c.Server = &ServerMethods{Client: c}
var err error
if c.conn, err = net.DialTimeout("tcp", addr, c.timeout); err != nil {
return nil, err
}
c.scanner = bufio.NewScanner(bufio.NewReader(c.conn))
c.scanner.Split(ScanLines)
if err := c.setDeadline(); err != nil {
return nil, err
}
// Reader the connection header
if !c.scanner.Scan() {
return nil, c.scanErr()
}
if l := c.scanner.Text(); l != connectHeader {
return nil, fmt.Errorf("invalid connection header %q", l)
}
// Slurp the banner
if !c.scanner.Scan() {
return nil, c.scanErr()
}
return c, nil
}
// setDeadline updates the deadline on the connection based on the clients configured timeout.
func (c *Client) setDeadline() error {
return c.conn.SetDeadline(time.Now().Add(c.timeout))
}
// Exec executes cmd on the server and returns the response.
func (c *Client) Exec(cmd string) ([]string, error) {
return c.ExecCmd(NewCmd(cmd))
}
// ExecCmd executes cmd on the server and returns the response.
func (c *Client) ExecCmd(cmd *Cmd) ([]string, error) {
if err := c.setDeadline(); err != nil {
return nil, err
}
if _, err := c.conn.Write([]byte(cmd.String())); err != nil {
return nil, err
}
if err := c.setDeadline(); err != nil {
return nil, err
}
lines := make([]string, 0, 10)
for c.scanner.Scan() {
l := c.scanner.Text()
if l == "error id=0 msg=ok" {
if cmd.response != nil {
if err := DecodeResponse(lines, cmd.response); err != nil {
return nil, err
}
}
return lines, nil
} else if matches := respTrailerRe.FindStringSubmatch(l); len(matches) == 4 {
return nil, NewError(matches)
} else {
lines = append(lines, l)
}
}
return nil, c.scanErr()
}
// Close closes the connection to the server.
func (c *Client) Close() error {
_, err := c.Exec("quit")
err2 := c.conn.Close()
if err != nil {
return err
}
return err2
}
// scanError returns the error from the scanner if non-nil,
// io.ErrUnexpectedEOF otherwise.
func (c *Client) scanErr() error {
if err := c.scanner.Err(); err != nil {
return err
}
return io.ErrUnexpectedEOF
}

189
client_test.go Normal file
Просмотреть файл

@ -0,0 +1,189 @@
package ts3
import (
"errors"
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestClient(t *testing.T) {
s := newServer(t)
if s == nil {
return
}
defer func() {
assert.NoError(t, s.Close())
}()
c, err := NewClient(s.Addr, Timeout(time.Second*2))
if !assert.NoError(t, err) {
return
}
defer func() {
assert.Error(t, c.Close())
}()
_, err = c.Exec("version")
assert.NoError(t, err)
_, err = c.ExecCmd(NewCmd("version"))
assert.NoError(t, err)
_, err = c.ExecCmd(NewCmd("invalid"))
assert.Error(t, err)
_, err = c.ExecCmd(NewCmd("disconnect"))
assert.Error(t, err)
}
func TestClientNilOption(t *testing.T) {
_, err := NewClient("", nil)
if !assert.Error(t, err) {
return
}
assert.Equal(t, ErrNilOption, err)
}
func TestClientOptionError(t *testing.T) {
errBadOption := errors.New("bad option")
_, err := NewClient("", func(c *Client) error { return errBadOption })
if !assert.Error(t, err) {
return
}
assert.Equal(t, errBadOption, err)
}
func TestClientDisconnect(t *testing.T) {
s := newServer(t)
if s == nil {
return
}
defer func() {
assert.NoError(t, s.Close())
}()
c, err := NewClient(s.Addr, Timeout(time.Second*2))
if !assert.NoError(t, err) {
return
}
assert.NoError(t, c.Close())
_, err = c.Exec("version")
assert.Error(t, err)
}
func TestClientWriteFail(t *testing.T) {
s := newServer(t)
if s == nil {
return
}
defer func() {
assert.NoError(t, s.Close())
}()
c, err := NewClient(s.Addr, Timeout(time.Second*2))
if !assert.NoError(t, err) {
return
}
assert.NoError(t, c.conn.(*net.TCPConn).CloseWrite())
_, err = c.Exec("version")
assert.Error(t, err)
}
func TestClientDialFail(t *testing.T) {
c, err := NewClient("127.0.0.1", Timeout(time.Nanosecond))
if assert.Error(t, err) {
return
}
// Should never get here
assert.NoError(t, c.Close())
}
func TestClientNoHeader(t *testing.T) {
s := newServerStopped(t)
if s == nil {
return
}
s.noHeader = true
s.Start()
defer func() {
assert.NoError(t, s.Close())
}()
c, err := NewClient(s.Addr, Timeout(time.Second))
if assert.Error(t, err) {
return
}
// Should never get here
assert.NoError(t, c.Close())
}
func TestClientNoBanner(t *testing.T) {
s := newServerStopped(t)
if s == nil {
return
}
s.noBanner = true
s.Start()
defer func() {
assert.NoError(t, s.Close())
}()
c, err := NewClient(s.Addr, Timeout(time.Second))
if assert.Error(t, err) {
return
}
// Should never get here
assert.NoError(t, c.Close())
}
func TestClientFailConn(t *testing.T) {
s := newServerStopped(t)
if s == nil {
return
}
s.failConn = true
s.Start()
defer func() {
assert.NoError(t, s.Close())
}()
c, err := NewClient(s.Addr, Timeout(time.Second))
if assert.Error(t, err) {
return
}
// Should never get here
assert.NoError(t, c.Close())
}
func TestClientBadHeader(t *testing.T) {
s := newServerStopped(t)
if s == nil {
return
}
s.badHeader = true
s.Start()
defer func() {
assert.NoError(t, s.Close())
}()
c, err := NewClient(s.Addr, Timeout(time.Second))
if assert.Error(t, err) {
return
}
// Should never get here
assert.NoError(t, c.Close())
}

109
cmd.go Normal file
Просмотреть файл

@ -0,0 +1,109 @@
package ts3
import (
"fmt"
"strings"
)
// Cmd represents a TeamSpeak 3 ServerQuery command.
type Cmd struct {
cmd string
args []CmdArg
options []string
response interface{}
}
// NewCmd creates a new Cmd.
func NewCmd(cmd string) *Cmd {
return &Cmd{cmd: cmd}
}
// WithArgs sets the command Args.
func (c *Cmd) WithArgs(args ...CmdArg) *Cmd {
c.args = args
return c
}
// WithOptions sets the command Options.
func (c *Cmd) WithOptions(options ...string) *Cmd {
c.options = options
return c
}
// WithResponse sets the command Response which will have the data returned from the server decoded into it.
func (c *Cmd) WithResponse(r interface{}) *Cmd {
c.response = r
return c
}
func (c *Cmd) String() string {
args := make([]interface{}, 1, len(c.args)+len(c.options)+1)
args[0] = c.cmd
for _, v := range c.args {
args = append(args, v.ArgString())
}
for _, v := range c.options {
args = append(args, v)
}
return fmt.Sprintln(args...)
}
// CmdArg is implemented by types which can be used as a command argument.
type CmdArg interface {
ArgString() string
}
// ArgGroup represents a group of TeamSpeak 3 ServerQuery command arguments.
type ArgGroup struct {
grp []CmdArg
}
// NewArgGroup returns a new ArgGroup.
func NewArgGroup(args ...CmdArg) *ArgGroup {
return &ArgGroup{grp: args}
}
// ArgString implements CmdArg.
func (ag *ArgGroup) ArgString() string {
args := make([]string, len(ag.grp))
for i, arg := range ag.grp {
args[i] = arg.ArgString()
}
return strings.Join(args, "|")
}
// ArgSet represents a set of TeamSpeak 3 ServerQuery command arguments.
type ArgSet struct {
set []CmdArg
}
// NewArgSet returns a new ArgSet.
func NewArgSet(args ...CmdArg) *ArgSet {
return &ArgSet{set: args}
}
// ArgString implements CmdArg.
func (ag *ArgSet) ArgString() string {
args := make([]string, len(ag.set))
for i, arg := range ag.set {
args[i] = arg.ArgString()
}
return strings.Join(args, " ")
}
// Arg represents a TeamSpeak 3 ServerQuery command argument.
// Args automatically escape white space and special characters before being sent to the server.
type Arg struct {
key string
val string
}
// NewArg returns a new Arg with key val.
func NewArg(key string, val interface{}) *Arg {
return &Arg{key: key, val: fmt.Sprint(val)}
}
// ArgString implements CmdArg
func (a *Arg) ArgString() string {
return fmt.Sprintf("%v=%v", encoder.Replace(a.key), encoder.Replace(a.val))
}

73
cmd_test.go Normal file
Просмотреть файл

@ -0,0 +1,73 @@
package ts3
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCmd(t *testing.T) {
tests := []struct {
name string
cmd *Cmd
expect string
}{
{"cmd", NewCmd("version"),
"version",
},
{"arg", NewCmd("use").
WithArgs(NewArg("sid", 1)),
"use sid=1",
},
{"args", NewCmd("use").
WithArgs(NewArg("sid", 1), NewArg("port", 1234)),
"use sid=1 port=1234",
},
{"args-option", NewCmd("use").
WithArgs(NewArg("sid", 1), NewArg("port", 1234)).
WithOptions("-virtual"),
"use sid=1 port=1234 -virtual",
},
{"options", NewCmd("serverlist").
WithOptions("-uid", "-short"),
"serverlist -uid -short",
},
{"arg-group-single", NewCmd("servergroupdelperm").
WithArgs(
NewArg("sgid", 1),
NewArgGroup(NewArg("permid", 1), NewArg("permid", 2)),
),
"servergroupdelperm sgid=1 permid=1|permid=2",
},
{"arg-group-multi", NewCmd("servergroupaddperm").
WithArgs(
NewArg("sgid", 1),
NewArgGroup(
NewArgSet(
NewArg("permid", 1),
NewArg("permvalue", 1),
NewArg("permnegated", 0),
NewArg("permskip", 0),
),
NewArgSet(
NewArg("permid", 2),
NewArg("permvalue", 2),
NewArg("permnegated", 1),
NewArg("permskip", 1),
),
),
),
"servergroupaddperm sgid=1 permid=1 permvalue=1 permnegated=0 permskip=0|permid=2 permvalue=2 permnegated=1 permskip=1",
},
{"escaped-chars", NewCmd("servergroupadd").
WithArgs(NewArg("name", "Chars:\\/ |\a\b\f\n\r\t\v")),
`servergroupadd name=Chars:\\\/\s\p\a\b\f\n\r\t\v`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expect+"\n", tc.cmd.String())
})
}
}

71
errors.go Normal file
Просмотреть файл

@ -0,0 +1,71 @@
package ts3
import (
"errors"
"fmt"
"strconv"
"strings"
)
var (
// ErrInvalidConnectHeader is returned by NewClient if the server doesn't respond with the required connection header.
ErrInvalidConnectHeader = errors.New("invalid connect header")
// ErrNilOption is returned by NewClient if an option is nil.
ErrNilOption = errors.New("nil option")
)
// Error represents a error returned from the TeamSpeak 3 server.
type Error struct {
ID int
Msg string
Details map[string]interface{}
}
// NewError returns a new Error parsed from TeamSpeak 3 server response.
func NewError(matches []string) *Error {
e := &Error{Msg: Decode(matches[2])}
var err error
if e.ID, err = strconv.Atoi(matches[1]); err != nil {
// This should be impossible given it matched \d+ in the regexp.
e.ID = -1
}
if rem := strings.TrimSpace(matches[3]); rem != "" {
e.Details = make(map[string]interface{})
for _, s := range strings.Split(rem, " ") {
d := strings.SplitN(s, "=", 2)
v := Decode(d[0])
if i, err := strconv.Atoi(d[1]); err == nil {
e.Details[v] = i
} else {
e.Details[v] = Decode(d[1])
}
}
}
return e
}
func (e *Error) Error() string {
if len(e.Details) > 0 {
return fmt.Sprintf("%v %v (%v)", e.Msg, e.Details, e.ID)
}
return fmt.Sprintf("%v (%v)", e.Msg, e.ID)
}
// InvalidResponseError is the error returned when the response data was invalid.
type InvalidResponseError struct {
Reason string
Data []string
}
// NewInvalidResponseError returns a new InvalidResponseError from lines.
func NewInvalidResponseError(reason string, lines []string) *InvalidResponseError {
return &InvalidResponseError{Reason: reason, Data: lines}
}
func (e *InvalidResponseError) Error() string {
return fmt.Sprintf("%v (%+v)", e.Reason, e.Data)
}

68
errors_test.go Normal file
Просмотреть файл

@ -0,0 +1,68 @@
package ts3
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewError(t *testing.T) {
tests := []struct {
name string
line string
expected *Error
}{
{"ok",
`error id=0 msg=ok`,
&Error{Msg: "ok"},
},
{"invalid-server",
`error id=1024 msg=invalid\sserverID`,
&Error{
ID: 1024,
Msg: "invalid serverID",
},
},
{"permission",
`error id=2568 msg=insufficient\sclient\spermissions failed_permid=4 other=test`,
&Error{
ID: 2568,
Msg: "insufficient client permissions",
Details: map[string]interface{}{"failed_permid": 4, "other": "test"},
},
},
{"invalid",
` error id=0 msg=ok`,
nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
matches := respTrailerRe.FindStringSubmatch(tc.line)
if tc.expected == nil {
assert.Equal(t, 0, len(matches))
return
}
if !assert.Equal(t, 4, len(matches)) {
return
}
err := NewError(matches)
assert.Error(t, err)
assert.Equal(t, tc.expected, err)
assert.NotEmpty(t, err.Error())
})
}
}
func TestNewInvalidResponseError(t *testing.T) {
reason := "my reason"
lines := []string{"line1"}
err := NewInvalidResponseError(reason, lines)
assert.Error(t, err)
assert.Contains(t, err.Error(), err.Reason)
assert.Contains(t, err.Error(), err.Data[0])
assert.Equal(t, reason, err.Reason)
assert.Equal(t, lines, err.Data)
}

141
helpers.go Normal file
Просмотреть файл

@ -0,0 +1,141 @@
package ts3
import (
"fmt"
"reflect"
"strconv"
"strings"
"github.com/mitchellh/mapstructure"
)
var (
// encoder performs white space and special character encoding
// as required by the ServerQuery protocol.
encoder = strings.NewReplacer(
`\`, `\\`,
`/`, `\/`,
` `, `\s`,
`|`, `\p`,
"\a", `\a`,
"\b", `\b`,
"\f", `\f`,
"\n", `\n`,
"\r", `\r`,
"\t", `\t`,
"\v", `\v`,
)
// decoder performs white space and special character decoding
// as required by the ServerQuery protocol.
decoder = strings.NewReplacer(
`\\`, "\\",
`\/`, "/",
`\s`, " ",
`\p`, "|",
`\a`, "\a",
`\b`, "\b",
`\f`, "\f",
`\n`, "\n",
`\r`, "\r",
`\t`, "\t",
`\v`, "\v",
)
)
// Decode returns a decoded version of str.
func Decode(str string) string {
return decoder.Replace(str)
}
// DecodeResponse decodes a response into a struct.
func DecodeResponse(lines []string, v interface{}) error {
if len(lines) != 1 {
return NewInvalidResponseError("too many lines", lines)
}
input := make(map[string]interface{})
value := reflect.ValueOf(v)
var slice reflect.Value
var elemType reflect.Type
if value.Kind() == reflect.Ptr {
slice = value.Elem()
if slice.Kind() == reflect.Slice {
elemType = slice.Type().Elem()
}
}
for _, part := range strings.Split(lines[0], "|") {
for _, val := range strings.Split(part, " ") {
parts := strings.SplitN(val, "=", 2)
// TODO(steve): support groups
key := Decode(parts[0])
if len(parts) == 2 {
v := Decode(parts[1])
if i, err := strconv.Atoi(v); err != nil {
input[key] = v
} else {
input[key] = i
}
} else {
input[key] = ""
}
}
if elemType != nil {
// Expecting a slice
if err := decodeSlice(elemType, slice, input); err != nil {
return err
}
// Reset the input map
input = make(map[string]interface{})
}
}
if elemType != nil {
// Expecting a slice, already decoded
return nil
}
return decodeMap(input, v)
}
// decodeMap decodes input into r.
func decodeMap(d map[string]interface{}, r interface{}) error {
cfg := &mapstructure.DecoderConfig{
WeaklyTypedInput: true,
TagName: "ms",
Result: r,
}
dec, err := mapstructure.NewDecoder(cfg)
if err != nil {
return err
}
return dec.Decode(d)
}
// decodeSlice decodes input into slice.
func decodeSlice(elemType reflect.Type, slice reflect.Value, input map[string]interface{}) error {
var v reflect.Value
if elemType.Kind() == reflect.Ptr {
v = reflect.New(elemType.Elem())
} else {
v = reflect.New(elemType)
}
if !v.CanInterface() {
return fmt.Errorf("can't interface %#v", v)
}
if err := decodeMap(input, v.Interface()); err != nil {
return err
}
if elemType.Kind() == reflect.Struct {
v = v.Elem()
}
slice.Set(reflect.Append(slice, v))
return nil
}

32
helpers_test.go Normal file
Просмотреть файл

@ -0,0 +1,32 @@
package ts3
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEncodeDecode(t *testing.T) {
str := "\\/ |\a\b\f\n\r\t\v"
assert.Equal(t, `\\\/\s\p\a\b\f\n\r\t\v`, encoder.Replace(str))
assert.Equal(t, str, Decode(encoder.Replace(str)))
}
type testResp struct {
Response string
ID int
Valid bool
}
func TestDecodeResponse(t *testing.T) {
r := &testResp{}
expected := &testResp{
Response: "test",
ID: 1,
Valid: false,
}
assert.NoError(t, DecodeResponse([]string{"response=test id=1 valid"}, r))
assert.Equal(t, expected, r)
assert.Error(t, DecodeResponse([]string{"line1", "line2"}, r))
}

233
mockserver_test.go Normal file
Просмотреть файл

@ -0,0 +1,233 @@
package ts3
import (
"bufio"
"net"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
const (
cmdQuit = "quit"
banner = `Welcome to the TeamSpeak 3 ServerQuery interface, type "help" for a list of commands and "help <command>" for information on a specific command.`
errUnknownCmd = `error id=256 msg=command\snot\sfound`
errOK = `error id=0 msg=ok`
)
var (
commands = map[string]string{
"version": "version=3.0.12.2 build=1455547898 platform=FreeBSD",
"login": "",
"logout": "",
"use": "",
"serverlist": `virtualserver_id=1 virtualserver_port=10677 virtualserver_status=online virtualserver_clientsonline=1 virtualserver_queryclientsonline=1 virtualserver_maxclients=35 virtualserver_uptime=12345025 virtualserver_name=Server\s#1 virtualserver_autostart=1 virtualserver_machine_id=1 virtualserver_unique_identifier=uniq1|virtualserver_id=2 virtualserver_port=10617 virtualserver_status=online virtualserver_clientsonline=3 virtualserver_queryclientsonline=2 virtualserver_maxclients=10 virtualserver_uptime=3165117 virtualserver_name=Server\s#2 virtualserver_autostart=1 virtualserver_machine_id=1 virtualserver_unique_identifier=uniq2`,
"serverinfo": `virtualserver_antiflood_points_needed_command_block=150 virtualserver_antiflood_points_needed_ip_block=250 virtualserver_antiflood_points_tick_reduce=5 virtualserver_channel_temp_delete_delay_default=0 virtualserver_codec_encryption_mode=0 virtualserver_complain_autoban_count=5 virtualserver_complain_autoban_time=1200 virtualserver_complain_remove_time=3600 virtualserver_created=0 virtualserver_default_channel_admin_group=1 virtualserver_default_channel_group=4 virtualserver_default_server_group=5 virtualserver_download_quota=18446744073709551615 virtualserver_filebase=files virtualserver_flag_password=0 virtualserver_hostbanner_gfx_interval=0 virtualserver_hostbanner_gfx_url virtualserver_hostbanner_mode=0 virtualserver_hostbanner_url virtualserver_hostbutton_gfx_url virtualserver_hostbutton_tooltip=Multiplay\sGame\sServers virtualserver_hostbutton_url=http:\/\/www.multiplaygameservers.com virtualserver_hostmessage virtualserver_hostmessage_mode=0 virtualserver_icon_id=0 virtualserver_log_channel=0 virtualserver_log_client=0 virtualserver_log_filetransfer=0 virtualserver_log_permissions=1 virtualserver_log_query=0 virtualserver_log_server=0 virtualserver_max_download_total_bandwidth=18446744073709551615 virtualserver_max_upload_total_bandwidth=18446744073709551615 virtualserver_maxclients=32 virtualserver_min_android_version=0 virtualserver_min_client_version=0 virtualserver_min_clients_in_channel_before_forced_silence=100 virtualserver_min_ios_version=0 virtualserver_name=Test\sServer virtualserver_name_phonetic virtualserver_needed_identity_security_level=8 virtualserver_password virtualserver_priority_speaker_dimm_modificator=-18.0000 virtualserver_reserved_slots=0 virtualserver_status=template virtualserver_unique_identifier virtualserver_upload_quota=18446744073709551615 virtualserver_weblist_enabled=1 virtualserver_welcomemessage=Welcome\sto\sTeamSpeak,\scheck\s[URL]www.teamspeak.com[\/URL]\sfor\slatest\sinfos.`,
"servercreate": `sid=2 virtualserver_port=9988 token=eKnFZQ9EK7G7MhtuQB6+N2B1PNZZ6OZL3ycDp2OW`,
"serveridgetbyport": `server_id=1`,
"servergrouplist": `sgid=1 name=Guest\sServer\sQuery type=2 iconid=0 savedb=0 sortid=0 namemode=0 n_modifyp=0 n_member_addp=0 n_member_removep=0|sgid=2 name=Admin\sServer\sQuery type=2 iconid=500 savedb=1 sortid=0 namemode=0 n_modifyp=100 n_member_addp=100 n_member_removep=100`,
"privilegekeylist": `token=zTfamFVhiMEzhTl49KrOVYaMilHPDQEBQOJFh6qX token_type=0 token_id1=17395 token_id2=0 token_created=1499948005 token_description`,
"privilegekeyadd": `token=zTfamFVhiMEzhTl49KrOVYaMilHPgQEBQOJFh6qX`,
"serverdelete": "",
"serverstop": "",
"serverstart": "",
"serveredit": "",
cmdQuit: "",
}
)
// newLockListener creates a new listener on the local IP.
// If listen fails it panics
func newLocalListener() (net.Listener, error) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
return nil, err
}
}
return l, nil
}
// server is a mock TeamSpeak 3 server
type server struct {
Addr string
Listener net.Listener
t *testing.T
conns map[net.Conn]struct{}
done chan struct{}
wg sync.WaitGroup
noHeader bool
noBanner bool
failConn bool
badHeader bool
mtx sync.Mutex
}
// sconn represents a server connection
type sconn struct {
id int
net.Conn
}
// newServer returns a running server or nil if an error occurred.
func newServer(t *testing.T) *server {
s := newServerStopped(t)
s.Start()
return s
}
// newServerStopped returns a stopped servers or nil if an error occurred.
func newServerStopped(t *testing.T) *server {
l, err := newLocalListener()
if !assert.NoError(t, err) {
return nil
}
s := &server{
Listener: l,
conns: make(map[net.Conn]struct{}),
done: make(chan struct{}),
t: t,
}
s.Addr = s.Listener.Addr().String()
return s
}
// Start starts the server.
func (s *server) Start() {
s.wg.Add(1)
go s.serve()
}
// server processes incoming requests until signaled to stop with Close.
func (s *server) serve() {
defer s.wg.Done()
for {
conn, err := s.Listener.Accept()
if err != nil {
if s.running() {
assert.NoError(s.t, err)
}
return
}
s.wg.Add(1)
s.conns[conn] = struct{}{}
go s.handle(conn)
}
}
// writeResponse writes the given msg followed by an error (ok) response.
// If msg is empty the only the error (ok) rsponse is sent.
func (s *server) writeResponse(c *sconn, msg string) error {
if msg != "" {
if err := s.write(c.Conn, msg); err != nil {
return err
}
}
return s.write(c.Conn, errOK)
}
// write writes msg to conn.
func (s *server) write(conn net.Conn, msg string) error {
_, err := conn.Write([]byte(msg + "\n\r"))
if s.running() {
assert.NoError(s.t, err)
}
return err
}
// running returns true unless Close has been called, false otherwise.
func (s *server) running() bool {
select {
case <-s.done:
return false
default:
return true
}
}
// handle handles a client connection.
func (s *server) handle(conn net.Conn) {
defer func() {
s.closeConn(conn)
s.wg.Done()
}()
if s.failConn {
return
}
sc := bufio.NewScanner(bufio.NewReader(conn))
sc.Split(bufio.ScanLines)
if !s.noHeader {
if s.badHeader {
if err := s.write(conn, "bad"); err != nil {
return
}
} else {
if err := s.write(conn, connectHeader); err != nil {
return
}
}
if !s.noBanner {
if err := s.write(conn, banner); err != nil {
return
}
}
}
c := &sconn{Conn: conn}
for sc.Scan() {
l := sc.Text()
parts := strings.Split(l, " ")
resp, ok := commands[parts[0]]
var err error
if ok {
err = s.writeResponse(c, resp)
} else if parts[0] == "disconnect" {
return
} else {
err = s.write(c, errUnknownCmd)
}
if err != nil || parts[0] == cmdQuit {
return
}
}
if err := sc.Err(); err != nil && s.running() {
assert.NoError(s.t, err)
}
}
// closeConn closes a client connection and removes it from our map of connections.
func (s *server) closeConn(conn net.Conn) {
s.mtx.Lock()
defer s.mtx.Unlock()
conn.Close() // nolint: errcheck
delete(s.conns, conn)
}
// Close cleanly shuts down the server.
func (s *server) Close() error {
close(s.done)
err := s.Listener.Close()
s.mtx.Lock()
for c := range s.conns {
if err2 := c.Close(); err2 != nil && err == nil {
err = err2
}
}
s.mtx.Unlock()
s.wg.Wait()
return err
}

29
scanner.go Normal file
Просмотреть файл

@ -0,0 +1,29 @@
package ts3
import (
"strings"
)
// ScanLines is a split function for a bytes.Scanner that returns each line of
// text, stripped of any trailing end-of-line marker. The returned line may
// be empty. The end-of-line marker is one newline followed by a carriage return.
// In regular expression notation, it is `\n\r`.
// The last non-empty line of input will be returned even if it has no newline.
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := strings.Index(string(data), "\n\r"); i >= 0 {
// We have a full end-of-line terminated line.
return i + 2, data[0:i], nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}

33
scanner_test.go Normal file
Просмотреть файл

@ -0,0 +1,33 @@
package ts3
import (
"bufio"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestScanLines(t *testing.T) {
str := "line1\n\rline2\n\rline3"
s := bufio.NewScanner(strings.NewReader(str))
s.Split(ScanLines)
lines := []string{
"line1",
"line2",
"line3",
}
var i int
for s.Scan() {
if !assert.True(t, len(lines) > i) {
return
}
assert.Equal(t, lines[i], s.Text())
i++
}
assert.Equal(t, len(lines), i)
}

215
server_cmds.go Normal file
Просмотреть файл

@ -0,0 +1,215 @@
package ts3
// ServerMethods groups server methods.
type ServerMethods struct {
*Client
}
// Server represents a TeamSpeak 3 virtual server.
type Server struct {
AnitFloodPointsNeededCommandBlock int `ms:"virtualserver_antiflood_points_needed_command_block"`
AnitFloodPointsNeededIPBlock int `ms:"virtualserver_antiflood_points_needed_ip_block"`
AnitFloodPointsTickReduce int `ms:"virtualserver_antiflood_points_tick_reduce"`
ChannelsOnline int `ms:"virtualserver_channelsonline"`
ClientsOnline int `ms:"virtualserver_clientsonline"`
CodecEncryptionMode int `ms:"virtualserver_codec_encryption_mode"`
ComplainAutoBanCount int `ms:"virtualserver_complain_autoban_count"`
ComplainAutoBanTime int `ms:"virtualserver_complain_autoban_time"`
ComplainRemoveTime int `ms:"virtualserver_complain_remove_time"`
Created int `ms:"virtualserver_created"`
DefaultChannelAdminGroup int `ms:"virtualserver_default_channel_admin_group"`
DefaultChannelGroup int `ms:"virtualserver_default_channel_group"`
DefaultServerGroup int `ms:"virtualserver_default_server_group"`
HostBannerGFXInterval int `ms:"virtualserver_hostbanner_gfx_interval"`
HostMessageMode int `ms:"virtualserver_hostmessage_mode"`
ID int `ms:"virtualserver_id"`
MachineID int `ms:"virtualserver_machine_id"`
MaxClients int `ms:"virtualserver_maxclients"`
MinClientsInChannelBeforeForcedSilence int `ms:"virtualserver_min_clients_in_channel_before_forced_silence"`
NeededIdentitySecurityLevel int `ms:"virtualserver_needed_identity_security_level"`
Port int `ms:"virtualserver_port"`
QueryClientsOnline int `ms:"virtualserver_queryclientsonline"`
Uptime int `ms:"virtualserver_uptime"` // TODO(steve): use time.Duration
AskForPrivilegeKey bool `ms:"virtualserver_ask_for_privilegekey"`
AutoStart bool `ms:"virtualserver_autostart"`
FlagPassword bool `ms:"virtualserver_flag_password"`
LogChannel bool `ms:"virtualserver_log_channel"`
LogClient bool `ms:"virtualserver_log_client"`
LogFileTransfer bool `ms:"virtualserver_log_filetransfer"`
LogPermissions bool `ms:"virtualserver_log_permissions"`
LogQuery bool `ms:"virtualserver_log_client"`
LogServer bool `ms:"virtualserver_log_server"`
WebListEnabled bool `ms:"virtualserver_web_list_enabled"`
PrioritySpeakerDimmModificator float32 `ms:"virtualserver_priority_speaker_dimm_modificator"`
BandwidthReceivedLastMinuteTotal int `ms:"virtualserver_bandwidth_received_last_minute_total"`
BandwidthReceivedLastSecondTotal int `ms:"virtualserver_bandwidth_received_last_second_total"`
BandwidthSentLastMinuteTotal int `ms:"virtualserver_bandwidth_sent_last_minute_total"`
BandwidthSentLastSecondTotal int `ms:"virtualserver_bandwidth_sent_last_second_total"`
ChannelTempDeleteDelayDefault int `ms:"virtualserver_channel_temp_delete_delay_default"`
HostBannerMode int `ms:"virtualserver_hostbanner_mode"`
IconID int `ms:"virtualserver_icon_id"`
MinAndroidVersion int `ms:"virtualserver_min_android_version"`
MinClientVersion int `ms:"virtualserver_min_client_version"`
MiniOSVersion int `ms:"virtualserver_min_ios_version"`
ReserverSlots int `ms:"virtualserver_reserved_slots"`
TotalPing int `ms:"virtualserver_total_ping"`
MaxDownloadTotalBandwidth uint64 `ms:"virtualserver_max_download_total_bandwidth"`
MaxUploadTotalBandwidth uint64 `ms:"virtualserver_max_upload_total_bandwidth"`
MonthBytesDownloaded int64 `ms:"virtualserver_MonthBytesDownloaded"`
MonthBytesUploaded int64 `ms:"virtualserver_MonthBytesUploaded"`
TotalBytesDownloaded int64 `ms:"virtualserver_TotalBytesDownloaded"`
TotalBytesUploaded int64 `ms:"virtualserver_TotalBytesUploaded"`
TotalPacketLossControl int64 `ms:"virtualserver_total_packet_loss_control"`
TotalPacketLossKeepalive int64 `ms:"virtualserver_total_packet_loss_keepalive"`
TotalPacketLossSpeech int64 `ms:"virtualserver_total_packet_loss_speech"`
TotalPacketLossTotal int64 `ms:"virtualserver_total_packet_loss_total"`
VirtualServerDownloadQuota int64 `ms:"virtualserver_virtualserver_download_quota"`
VirtualServerUploadQuota int64 `ms:"virtualserver_virtualserver_Upload_quota"`
FileBase string `ms:"virtualserver_filebase"`
HostBannerGFXURL string `ms:"virtualserver_hostbanner_gfx_url"`
HostBannerURL string `ms:"virtualserver_hostbanner_url"`
HostButtonGFXURL string `ms:"virtualserver_hostbutton_gfx_url"`
HostButtonToolTip string `ms:"virtualserver_hostbutton_tooltip"`
HostButtonURL string `ms:"virtualserver_hostbutton_url"`
HostMessage string `ms:"virtualserver_hostmessage"`
Name string `ms:"virtualserver_name"`
NamePhonetic string `ms:"virtualserver_name_phonetic"`
Password string `ms:"virtualserver_password"`
Platform string `ms:"virtualserver_platform"`
Status string `ms:"virtualserver_status"`
UniqueIdentifier string `ms:"virtualserver_unique_identifier"`
Version string `ms:"virtualserver_version"`
WelcomeMessage string `ms:"virtualserver_welcomemessage"`
}
// List lists virtual servers.
func (s *ServerMethods) List(options ...string) ([]*Server, error) {
var servers []*Server
if _, err := s.ExecCmd(NewCmd("serverlist").WithOptions(options...).WithResponse(&servers)); err != nil {
return nil, err
}
return servers, nil
}
// IDGetByPort returns the database id of the virtual server running on UDP port.
func (s *ServerMethods) IDGetByPort(port uint16) (int, error) {
r := struct {
ID int `ms:"server_id"`
}{}
_, err := s.ExecCmd(NewCmd("serveridgetbyport").WithArgs(NewArg("virtualserver_port", port)).WithResponse(&r))
return r.ID, err
}
// Info returns detailed configuration information about the selected server.
func (s *ServerMethods) Info() (*Server, error) {
r := &Server{}
if _, err := s.ExecCmd(NewCmd("serverinfo").WithResponse(&r)); err != nil {
return nil, err
}
return r, nil
}
// Edit changes the selected virtual servers configuration using the given args.
func (s *ServerMethods) Edit(args ...CmdArg) error {
_, err := s.ExecCmd(NewCmd("serveredit").WithArgs(args...))
return err
}
// Delete deletes the virtual server specified by id.
// Only virtual server in a stopped state can be deleted.
func (s *ServerMethods) Delete(id int) error {
_, err := s.ExecCmd(NewCmd("serverdelete").WithArgs(NewArg("sid", id)))
return err
}
// CreatedServer is the details returned by a server create.
type CreatedServer struct {
ID int `ms:"sid"`
Port uint16 `ms:"virtualserver_port"`
Token string
}
// Create creates a new virtual server using the given properties and returns
// its ID, port and initial administrator privilege key.
// If virtualserver_port arg is not specified, the server will use the first unused
// UDP port.
func (s *ServerMethods) Create(name string, args ...CmdArg) (*CreatedServer, error) {
r := &CreatedServer{}
args = append(args, NewArg("virtualserver_name", name))
if _, err := s.ExecCmd(NewCmd("servercreate").WithArgs(args...).WithResponse(r)); err != nil {
return nil, err
}
return r, nil
}
// Start starts the virtual server specified by id.
func (s *ServerMethods) Start(id int) error {
_, err := s.ExecCmd(NewCmd("serverstart").WithArgs(NewArg("sid", id)))
return err
}
// Stop stops the virtual server specified by id.
func (s *ServerMethods) Stop(id int) error {
_, err := s.ExecCmd(NewCmd("serverstop").WithArgs(NewArg("sid", id)))
return err
}
// Group represents a virtual server group.
type Group struct {
ID int `ms:"sgid"`
Name string
Type int
IconID int
Saved bool `ms:"savedb"`
SortID int
NameMode int
ModifyPower int `ms:"n_modifyp"`
MemberAddPower int `ms:"n_member_addp"`
MemberRemovePower int `ms:"n_member_addp"`
}
// GroupList returns a list of available groups for the selected server.
func (s *ServerMethods) GroupList() ([]*Group, error) {
var groups []*Group
if _, err := s.ExecCmd(NewCmd("servergrouplist").WithResponse(&groups)); err != nil {
return nil, err
}
return groups, nil
}
// PrivilegeKey represents a server privilege key.
type PrivilegeKey struct {
Token string
Type int `ms:"token_type"`
ID1 int `ms:"token_id1"`
ID2 int `ms:"token_id2"`
Created int `ms:"token_created"`
Description string `ms:"token_description"`
}
// PrivilegeKeyList returns a list of available privilege keys for the selected server,
// including their type and group IDs.
func (s *ServerMethods) PrivilegeKeyList() ([]*PrivilegeKey, error) {
var keys []*PrivilegeKey
if _, err := s.ExecCmd(NewCmd("privilegekeylist").WithResponse(&keys)); err != nil {
return nil, err
}
return keys, nil
}
// PrivilegeKeyAdd creates a new privilege token to the selected server and returns it.
// If tokentype is set to 0, the ID specified with id1 will be a server group ID.
// Otherwise, id1 is used as a channel group ID and you need to provide a valid channel ID using id2.
func (s *ServerMethods) PrivilegeKeyAdd(ttype, id1, id2 int, options ...CmdArg) (string, error) {
t := struct {
Token string
}{}
options = append(options, NewArg("tokentype", ttype), NewArg("tokenid1", id1), NewArg("tokenid2", id2))
_, err := s.ExecCmd(NewCmd("privilegekeylist").WithArgs(options...).WithResponse(&t))
return t.Token, err
}

203
server_cmds_test.go Normal file
Просмотреть файл

@ -0,0 +1,203 @@
package ts3
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCmdsServer(t *testing.T) {
s := newServer(t)
if s == nil {
return
}
defer func() {
assert.NoError(t, s.Close())
}()
c, err := NewClient(s.Addr, Timeout(time.Second*2))
if !assert.NoError(t, err) {
return
}
defer func() {
assert.NoError(t, c.Close())
}()
list := func(t *testing.T) {
servers, err := c.Server.List()
if !assert.NoError(t, err) {
return
}
expected := []*Server{
&Server{
ID: 1,
Port: 10677,
Status: "online",
ClientsOnline: 1,
QueryClientsOnline: 1,
MaxClients: 35,
Uptime: 12345025,
Name: "Server #1",
AutoStart: true,
MachineID: 1,
UniqueIdentifier: "uniq1",
},
&Server{
ID: 2,
Port: 10617,
Status: "online",
ClientsOnline: 3,
QueryClientsOnline: 2,
MaxClients: 10,
Uptime: 3165117,
Name: "Server #2",
AutoStart: true,
MachineID: 1,
UniqueIdentifier: "uniq2",
},
}
assert.Equal(t, expected, servers)
}
idgetbyport := func(t *testing.T) {
id, err := c.Server.IDGetByPort(1234)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, 1, id)
}
info := func(t *testing.T) {
s, err := c.Server.Info()
if !assert.NoError(t, err) {
return
}
expected := &Server{
Status: "template",
MaxClients: 32,
Name: "Test Server",
AnitFloodPointsNeededCommandBlock: 150,
AnitFloodPointsNeededIPBlock: 250,
AnitFloodPointsTickReduce: 5,
ComplainAutoBanCount: 5,
ComplainAutoBanTime: 1200,
ComplainRemoveTime: 3600,
DefaultChannelAdminGroup: 1,
DefaultChannelGroup: 4,
DefaultServerGroup: 5,
MinClientsInChannelBeforeForcedSilence: 100,
NeededIdentitySecurityLevel: 8,
LogPermissions: true,
PrioritySpeakerDimmModificator: -18,
MaxDownloadTotalBandwidth: 18446744073709551615,
MaxUploadTotalBandwidth: 18446744073709551615,
FileBase: "files",
HostButtonToolTip: "Multiplay Game Servers",
HostButtonURL: "http://www.multiplaygameservers.com",
WelcomeMessage: "Welcome to TeamSpeak, check [URL]www.teamspeak.com[/URL] for latest infos.",
}
assert.Equal(t, expected, s)
}
create := func(t *testing.T) {
s, err := c.Server.Create("my server")
if !assert.NoError(t, err) {
return
}
expected := &CreatedServer{
ID: 2,
Port: 9988,
Token: "eKnFZQ9EK7G7MhtuQB6+N2B1PNZZ6OZL3ycDp2OW",
}
assert.Equal(t, expected, s)
}
edit := func(t *testing.T) {
assert.NoError(t, c.Server.Edit(NewArg("virtualserver_maxclients", 10)))
}
del := func(t *testing.T) {
assert.NoError(t, c.Server.Delete(1))
}
start := func(t *testing.T) {
assert.NoError(t, c.Server.Start(1))
}
stop := func(t *testing.T) {
assert.NoError(t, c.Server.Stop(1))
}
grouplist := func(t *testing.T) {
groups, err := c.Server.GroupList()
if !assert.NoError(t, err) {
return
}
expected := []*Group{
&Group{
ID: 1,
Name: "Guest Server Query",
Type: 2,
},
&Group{
ID: 2,
Name: "Admin Server Query",
Type: 2,
IconID: 500,
Saved: true,
ModifyPower: 100,
MemberAddPower: 100,
MemberRemovePower: 100,
},
}
assert.Equal(t, expected, groups)
}
privilegekeylist := func(t *testing.T) {
keys, err := c.Server.PrivilegeKeyList()
if !assert.NoError(t, err) {
return
}
expected := []*PrivilegeKey{
&PrivilegeKey{
Token: "zTfamFVhiMEzhTl49KrOVYaMilHPDQEBQOJFh6qX",
ID1: 17395,
Created: 1499948005,
},
}
assert.Equal(t, expected, keys)
}
privilegekeyadd := func(t *testing.T) {
token, err := c.Server.PrivilegeKeyAdd(0, 17395, 0)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, "zTfamFVhiMEzhTl49KrOVYaMilHPDQEBQOJFh6qX", token)
}
tests := []struct {
name string
f func(t *testing.T)
}{
{"list", list},
{"idgetbyport", idgetbyport},
{"info", info},
{"create", create},
{"edit", edit},
{"del", del},
{"start", start},
{"stop", stop},
{"grouplist", grouplist},
{"privilegekeylist", privilegekeylist},
{"privilegekeyadd", privilegekeyadd},
}
for _, tc := range tests {
t.Run(tc.name, tc.f)
}
}