Add plugin feature
Fixed database migration Added a plugin system based on the go plugin package
This commit is contained in:
parent
06d13d2bee
commit
e5b24f4c92
9
Makefile
9
Makefile
|
|
@ -14,14 +14,7 @@ test-race:
|
||||||
go test -v -race ./...
|
go test -v -race ./...
|
||||||
|
|
||||||
test-coverage:
|
test-coverage:
|
||||||
echo "" > coverage.txt
|
go test -v -coverprofile=coverage.txt -covermode=atomic ./...
|
||||||
for d in $(shell go list ./... | grep -v vendor); do \
|
|
||||||
go test -v -coverprofile=profile.out -covermode=atomic $$d ; \
|
|
||||||
if [ -f profile.out ]; then \
|
|
||||||
cat profile.out >> coverage.txt ; \
|
|
||||||
rm profile.out ; \
|
|
||||||
fi \
|
|
||||||
done
|
|
||||||
|
|
||||||
format:
|
format:
|
||||||
goimports -w $(shell find . -type f -name '*.go' -not -path "./vendor/*")
|
goimports -w $(shell find . -type f -name '*.go' -not -path "./vendor/*")
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,9 @@ type ApplicationAPI struct {
|
||||||
func (a *ApplicationAPI) CreateApplication(ctx *gin.Context) {
|
func (a *ApplicationAPI) CreateApplication(ctx *gin.Context) {
|
||||||
app := model.Application{}
|
app := model.Application{}
|
||||||
if err := ctx.Bind(&app); err == nil {
|
if err := ctx.Bind(&app); err == nil {
|
||||||
app.Token = generateNotExistingToken(auth.GenerateApplicationToken, a.applicationExists)
|
app.Token = auth.GenerateNotExistingToken(auth.GenerateApplicationToken, a.applicationExists)
|
||||||
app.UserID = auth.GetUserID(ctx)
|
app.UserID = auth.GetUserID(ctx)
|
||||||
|
app.Internal = false
|
||||||
a.DB.CreateApplication(&app)
|
a.DB.CreateApplication(&app)
|
||||||
ctx.JSON(200, withAbsoluteURL(ctx, &app))
|
ctx.JSON(200, withAbsoluteURL(ctx, &app))
|
||||||
}
|
}
|
||||||
|
|
@ -143,6 +144,10 @@ func (a *ApplicationAPI) GetApplications(ctx *gin.Context) {
|
||||||
func (a *ApplicationAPI) DeleteApplication(ctx *gin.Context) {
|
func (a *ApplicationAPI) DeleteApplication(ctx *gin.Context) {
|
||||||
withID(ctx, "id", func(id uint) {
|
withID(ctx, "id", func(id uint) {
|
||||||
if app := a.DB.GetApplicationByID(id); app != nil && app.UserID == auth.GetUserID(ctx) {
|
if app := a.DB.GetApplicationByID(id); app != nil && app.UserID == auth.GetUserID(ctx) {
|
||||||
|
if app.Internal {
|
||||||
|
ctx.AbortWithError(400, errors.New("cannot delete internal application"))
|
||||||
|
return
|
||||||
|
}
|
||||||
a.DB.DeleteApplicationByID(id)
|
a.DB.DeleteApplicationByID(id)
|
||||||
if app.Image != "" {
|
if app.Image != "" {
|
||||||
os.Remove(a.ImageDir + app.Image)
|
os.Remove(a.ImageDir + app.Image)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/gotify/server/mode"
|
"github.com/gotify/server/mode"
|
||||||
"github.com/gotify/server/model"
|
"github.com/gotify/server/model"
|
||||||
"github.com/gotify/server/test"
|
"github.com/gotify/server/test"
|
||||||
|
"github.com/gotify/server/test/testdb"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
@ -31,7 +32,7 @@ func TestApplicationSuite(t *testing.T) {
|
||||||
|
|
||||||
type ApplicationSuite struct {
|
type ApplicationSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
db *test.Database
|
db *testdb.Database
|
||||||
a *ApplicationAPI
|
a *ApplicationAPI
|
||||||
ctx *gin.Context
|
ctx *gin.Context
|
||||||
recorder *httptest.ResponseRecorder
|
recorder *httptest.ResponseRecorder
|
||||||
|
|
@ -41,7 +42,7 @@ func (s *ApplicationSuite) BeforeTest(suiteName, testName string) {
|
||||||
mode.Set(mode.TestDev)
|
mode.Set(mode.TestDev)
|
||||||
rand.Seed(50)
|
rand.Seed(50)
|
||||||
s.recorder = httptest.NewRecorder()
|
s.recorder = httptest.NewRecorder()
|
||||||
s.db = test.NewDB(s.T())
|
s.db = testdb.NewDB(s.T())
|
||||||
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
||||||
withURL(s.ctx, "http", "example.com")
|
withURL(s.ctx, "http", "example.com")
|
||||||
s.a = &ApplicationAPI{DB: s.db}
|
s.a = &ApplicationAPI{DB: s.db}
|
||||||
|
|
@ -76,8 +77,9 @@ func (s *ApplicationSuite) Test_ensureApplicationHasCorrectJsonRepresentation()
|
||||||
Name: "myapp",
|
Name: "myapp",
|
||||||
Description: "mydesc",
|
Description: "mydesc",
|
||||||
Image: "asd",
|
Image: "asd",
|
||||||
|
Internal: true,
|
||||||
}
|
}
|
||||||
test.JSONEquals(s.T(), actual, `{"id":1,"token":"Aasdasfgeeg","name":"myapp","description":"mydesc", "image": "asd"}`)
|
test.JSONEquals(s.T(), actual, `{"id":1,"token":"Aasdasfgeeg","name":"myapp","description":"mydesc", "image": "asd", "internal":true}`)
|
||||||
}
|
}
|
||||||
func (s *ApplicationSuite) Test_CreateApplication_expectBadRequestOnEmptyName() {
|
func (s *ApplicationSuite) Test_CreateApplication_expectBadRequestOnEmptyName() {
|
||||||
s.db.User(5)
|
s.db.User(5)
|
||||||
|
|
@ -183,6 +185,18 @@ func (s *ApplicationSuite) Test_GetApplications_WithImage() {
|
||||||
test.BodyEquals(s.T(), []*model.Application{first, second}, s.recorder)
|
test.BodyEquals(s.T(), []*model.Application{first, second}, s.recorder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ApplicationSuite) Test_DeleteApplication_internal_expectBadRequest() {
|
||||||
|
s.db.User(5).InternalApp(10)
|
||||||
|
|
||||||
|
test.WithUser(s.ctx, 5)
|
||||||
|
s.ctx.Request = httptest.NewRequest("DELETE", "/token/"+firstApplicationToken, nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: "10"}}
|
||||||
|
|
||||||
|
s.a.DeleteApplication(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ApplicationSuite) Test_DeleteApplication_expectNotFound() {
|
func (s *ApplicationSuite) Test_DeleteApplication_expectNotFound() {
|
||||||
s.db.User(5)
|
s.db.User(5)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ type ClientAPI struct {
|
||||||
func (a *ClientAPI) CreateClient(ctx *gin.Context) {
|
func (a *ClientAPI) CreateClient(ctx *gin.Context) {
|
||||||
client := model.Client{}
|
client := model.Client{}
|
||||||
if err := ctx.Bind(&client); err == nil {
|
if err := ctx.Bind(&client); err == nil {
|
||||||
client.Token = generateNotExistingToken(auth.GenerateClientToken, a.clientExists)
|
client.Token = auth.GenerateNotExistingToken(auth.GenerateClientToken, a.clientExists)
|
||||||
client.UserID = auth.GetUserID(ctx)
|
client.UserID = auth.GetUserID(ctx)
|
||||||
a.DB.CreateClient(&client)
|
a.DB.CreateClient(&client)
|
||||||
ctx.JSON(200, client)
|
ctx.JSON(200, client)
|
||||||
|
|
@ -145,12 +145,3 @@ func (a *ClientAPI) DeleteClient(ctx *gin.Context) {
|
||||||
func (a *ClientAPI) clientExists(token string) bool {
|
func (a *ClientAPI) clientExists(token string) bool {
|
||||||
return a.DB.GetClientByToken(token) != nil
|
return a.DB.GetClientByToken(token) != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateNotExistingToken(generateToken func() string, tokenExists func(token string) bool) string {
|
|
||||||
for {
|
|
||||||
token := generateToken()
|
|
||||||
if !tokenExists(token) {
|
|
||||||
return token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/gotify/server/mode"
|
"github.com/gotify/server/mode"
|
||||||
"github.com/gotify/server/model"
|
"github.com/gotify/server/model"
|
||||||
"github.com/gotify/server/test"
|
"github.com/gotify/server/test"
|
||||||
|
"github.com/gotify/server/test/testdb"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
@ -26,7 +27,7 @@ func TestClientSuite(t *testing.T) {
|
||||||
|
|
||||||
type ClientSuite struct {
|
type ClientSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
db *test.Database
|
db *testdb.Database
|
||||||
a *ClientAPI
|
a *ClientAPI
|
||||||
ctx *gin.Context
|
ctx *gin.Context
|
||||||
recorder *httptest.ResponseRecorder
|
recorder *httptest.ResponseRecorder
|
||||||
|
|
@ -37,7 +38,7 @@ func (s *ClientSuite) BeforeTest(suiteName, testName string) {
|
||||||
mode.Set(mode.TestDev)
|
mode.Set(mode.TestDev)
|
||||||
rand.Seed(50)
|
rand.Seed(50)
|
||||||
s.recorder = httptest.NewRecorder()
|
s.recorder = httptest.NewRecorder()
|
||||||
s.db = test.NewDB(s.T())
|
s.db = testdb.NewDB(s.T())
|
||||||
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
||||||
withURL(s.ctx, "http", "example.com")
|
withURL(s.ctx, "http", "example.com")
|
||||||
s.notified = false
|
s.notified = false
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/gotify/server/mode"
|
"github.com/gotify/server/mode"
|
||||||
"github.com/gotify/server/model"
|
"github.com/gotify/server/model"
|
||||||
"github.com/gotify/server/test"
|
"github.com/gotify/server/test"
|
||||||
|
"github.com/gotify/server/test/testdb"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
@ -22,7 +23,7 @@ func TestMessageSuite(t *testing.T) {
|
||||||
|
|
||||||
type MessageSuite struct {
|
type MessageSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
db *test.Database
|
db *testdb.Database
|
||||||
a *MessageAPI
|
a *MessageAPI
|
||||||
ctx *gin.Context
|
ctx *gin.Context
|
||||||
recorder *httptest.ResponseRecorder
|
recorder *httptest.ResponseRecorder
|
||||||
|
|
@ -34,7 +35,7 @@ func (s *MessageSuite) BeforeTest(suiteName, testName string) {
|
||||||
s.recorder = httptest.NewRecorder()
|
s.recorder = httptest.NewRecorder()
|
||||||
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
||||||
s.ctx.Request = httptest.NewRequest("GET", "/irrelevant", nil)
|
s.ctx.Request = httptest.NewRequest("GET", "/irrelevant", nil)
|
||||||
s.db = test.NewDB(s.T())
|
s.db = testdb.NewDB(s.T())
|
||||||
s.notifiedMessage = nil
|
s.notifiedMessage = nil
|
||||||
s.a = &MessageAPI{DB: s.db, Notifier: s}
|
s.a = &MessageAPI{DB: s.db, Notifier: s}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,395 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/gotify/location"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-yaml/yaml"
|
||||||
|
"github.com/gotify/server/auth"
|
||||||
|
"github.com/gotify/server/model"
|
||||||
|
"github.com/gotify/server/plugin"
|
||||||
|
"github.com/gotify/server/plugin/compat"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The PluginDatabase interface for encapsulating database access.
|
||||||
|
type PluginDatabase interface {
|
||||||
|
GetPluginConfByUser(userid uint) []*model.PluginConf
|
||||||
|
UpdatePluginConf(p *model.PluginConf) error
|
||||||
|
GetPluginConfByID(id uint) *model.PluginConf
|
||||||
|
}
|
||||||
|
|
||||||
|
// The PluginAPI provides handlers for managing plugins.
|
||||||
|
type PluginAPI struct {
|
||||||
|
Notifier Notifier
|
||||||
|
Manager *plugin.Manager
|
||||||
|
DB PluginDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPlugins returns all plugins a user has.
|
||||||
|
// swagger:operation GET /plugin plugin getPlugins
|
||||||
|
//
|
||||||
|
// Return all plugins.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// consumes: [application/json]
|
||||||
|
// produces: [application/json]
|
||||||
|
// security: [clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
|
||||||
|
// responses:
|
||||||
|
// 200:
|
||||||
|
// description: Ok
|
||||||
|
// schema:
|
||||||
|
// type: array
|
||||||
|
// items:
|
||||||
|
// $ref: "#/definitions/PluginConf"
|
||||||
|
// 401:
|
||||||
|
// description: Unauthorized
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 403:
|
||||||
|
// description: Forbidden
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 404:
|
||||||
|
// description: Not Found
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 500:
|
||||||
|
// description: Internal Server Error
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
func (c *PluginAPI) GetPlugins(ctx *gin.Context) {
|
||||||
|
userID := auth.GetUserID(ctx)
|
||||||
|
plugins := c.DB.GetPluginConfByUser(userID)
|
||||||
|
result := make([]model.PluginConfExternal, 0)
|
||||||
|
for _, conf := range plugins {
|
||||||
|
if inst, err := c.Manager.Instance(conf.ID); err == nil {
|
||||||
|
info := c.Manager.PluginInfo(conf.ModulePath)
|
||||||
|
result = append(result, model.PluginConfExternal{
|
||||||
|
ID: conf.ID,
|
||||||
|
Name: info.String(),
|
||||||
|
Token: conf.Token,
|
||||||
|
ModulePath: conf.ModulePath,
|
||||||
|
Author: info.Author,
|
||||||
|
Website: info.Website,
|
||||||
|
License: info.License,
|
||||||
|
Enabled: conf.Enabled,
|
||||||
|
Capabilities: inst.Supports().Strings(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.JSON(200, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnablePlugin enables a plugin.
|
||||||
|
// swagger:operation POST /plugin/:id/enable plugin enablePlugin
|
||||||
|
//
|
||||||
|
// Enable a plugin.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// consumes: [application/json]
|
||||||
|
// produces: [application/json]
|
||||||
|
// parameters:
|
||||||
|
// - name: id
|
||||||
|
// in: path
|
||||||
|
// description: the plugin id
|
||||||
|
// required: true
|
||||||
|
// type: integer
|
||||||
|
// security: [clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
|
||||||
|
// responses:
|
||||||
|
// 200:
|
||||||
|
// description: Ok
|
||||||
|
// 401:
|
||||||
|
// description: Unauthorized
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 403:
|
||||||
|
// description: Forbidden
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 404:
|
||||||
|
// description: Not Found
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 500:
|
||||||
|
// description: Internal Server Error
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
func (c *PluginAPI) EnablePlugin(ctx *gin.Context) {
|
||||||
|
withID(ctx, "id", func(id uint) {
|
||||||
|
conf := c.DB.GetPluginConfByID(id)
|
||||||
|
if conf == nil || !isPluginOwner(ctx, conf) {
|
||||||
|
ctx.AbortWithError(404, errors.New("unknown plugin"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err := c.Manager.Instance(id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(404, errors.New("plugin instance not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := c.Manager.SetPluginEnabled(id, true); err == plugin.ErrAlreadyEnabledOrDisabled {
|
||||||
|
ctx.AbortWithError(400, err)
|
||||||
|
} else if err != nil {
|
||||||
|
ctx.AbortWithError(500, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisablePlugin disables a plugin.
|
||||||
|
// swagger:operation POST /plugin/:id/disable plugin disablePlugin
|
||||||
|
//
|
||||||
|
// Disable a plugin.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// consumes: [application/json]
|
||||||
|
// produces: [application/json]
|
||||||
|
// parameters:
|
||||||
|
// - name: id
|
||||||
|
// in: path
|
||||||
|
// description: the plugin id
|
||||||
|
// required: true
|
||||||
|
// type: integer
|
||||||
|
// security: [clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
|
||||||
|
// responses:
|
||||||
|
// 200:
|
||||||
|
// description: Ok
|
||||||
|
// 401:
|
||||||
|
// description: Unauthorized
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 403:
|
||||||
|
// description: Forbidden
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 404:
|
||||||
|
// description: Not Found
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 500:
|
||||||
|
// description: Internal Server Error
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
func (c *PluginAPI) DisablePlugin(ctx *gin.Context) {
|
||||||
|
withID(ctx, "id", func(id uint) {
|
||||||
|
conf := c.DB.GetPluginConfByID(id)
|
||||||
|
if conf == nil || !isPluginOwner(ctx, conf) {
|
||||||
|
ctx.AbortWithError(404, errors.New("unknown plugin"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err := c.Manager.Instance(id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(404, errors.New("plugin instance not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := c.Manager.SetPluginEnabled(id, false); err == plugin.ErrAlreadyEnabledOrDisabled {
|
||||||
|
ctx.AbortWithError(400, err)
|
||||||
|
} else if err != nil {
|
||||||
|
ctx.AbortWithError(500, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDisplay get display info for Displayer plugin.
|
||||||
|
// swagger:operation GET /plugin/:id/display plugin getPluginDisplay
|
||||||
|
//
|
||||||
|
// Get display info for a Displayer plugin.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// consumes: [application/json]
|
||||||
|
// produces: [application/json]
|
||||||
|
// parameters:
|
||||||
|
// - name: id
|
||||||
|
// in: path
|
||||||
|
// description: the plugin id
|
||||||
|
// required: true
|
||||||
|
// type: integer
|
||||||
|
// security: [clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
|
||||||
|
// responses:
|
||||||
|
// 200:
|
||||||
|
// description: Ok
|
||||||
|
// schema:
|
||||||
|
// type: string
|
||||||
|
// 401:
|
||||||
|
// description: Unauthorized
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 403:
|
||||||
|
// description: Forbidden
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 404:
|
||||||
|
// description: Not Found
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 500:
|
||||||
|
// description: Internal Server Error
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
func (c *PluginAPI) GetDisplay(ctx *gin.Context) {
|
||||||
|
withID(ctx, "id", func(id uint) {
|
||||||
|
conf := c.DB.GetPluginConfByID(id)
|
||||||
|
if conf == nil || !isPluginOwner(ctx, conf) {
|
||||||
|
ctx.AbortWithError(404, errors.New("unknown plugin"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
instance, err := c.Manager.Instance(id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(404, errors.New("plugin instance not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(200, instance.GetDisplay(location.Get(ctx)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig returns Configurer plugin configuration in YAML format.
|
||||||
|
// swagger:operation GET /plugin/:id/config plugin getPluginConfig
|
||||||
|
//
|
||||||
|
// Get YAML configuration for Configurer plugin.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// consumes: [application/json]
|
||||||
|
// produces: [application/x-yaml]
|
||||||
|
// parameters:
|
||||||
|
// - name: id
|
||||||
|
// in: path
|
||||||
|
// description: the plugin id
|
||||||
|
// required: true
|
||||||
|
// type: integer
|
||||||
|
// security: [clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
|
||||||
|
// responses:
|
||||||
|
// 200:
|
||||||
|
// description: Ok
|
||||||
|
// schema:
|
||||||
|
// type: object
|
||||||
|
// description: plugin configuration
|
||||||
|
// 400:
|
||||||
|
// description: Bad Request
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 401:
|
||||||
|
// description: Unauthorized
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 403:
|
||||||
|
// description: Forbidden
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 404:
|
||||||
|
// description: Not Found
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 500:
|
||||||
|
// description: Internal Server Error
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
func (c *PluginAPI) GetConfig(ctx *gin.Context) {
|
||||||
|
withID(ctx, "id", func(id uint) {
|
||||||
|
conf := c.DB.GetPluginConfByID(id)
|
||||||
|
if conf == nil || !isPluginOwner(ctx, conf) {
|
||||||
|
ctx.AbortWithError(404, errors.New("unknown plugin"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
instance, err := c.Manager.Instance(id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(404, errors.New("plugin instance not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if aborted := supportOrAbort(ctx, instance, compat.Configurer); aborted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Header("content-type", "application/x-yaml")
|
||||||
|
ctx.Writer.Write(conf.Config)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateConfig updates Configurer plugin configuration in YAML format.
|
||||||
|
// swagger:operation POST /plugin/:id/config plugin updatePluginConfig
|
||||||
|
//
|
||||||
|
// Update YAML configuration for Configurer plugin.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// consumes: [application/x-yaml]
|
||||||
|
// produces: [application/json]
|
||||||
|
// parameters:
|
||||||
|
// - name: id
|
||||||
|
// in: path
|
||||||
|
// description: the plugin id
|
||||||
|
// required: true
|
||||||
|
// type: integer
|
||||||
|
// security: [clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
|
||||||
|
// responses:
|
||||||
|
// 200:
|
||||||
|
// description: Ok
|
||||||
|
// 400:
|
||||||
|
// description: Bad Request
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 401:
|
||||||
|
// description: Unauthorized
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 403:
|
||||||
|
// description: Forbidden
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 404:
|
||||||
|
// description: Not Found
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 500:
|
||||||
|
// description: Internal Server Error
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
func (c *PluginAPI) UpdateConfig(ctx *gin.Context) {
|
||||||
|
withID(ctx, "id", func(id uint) {
|
||||||
|
conf := c.DB.GetPluginConfByID(id)
|
||||||
|
if conf == nil || !isPluginOwner(ctx, conf) {
|
||||||
|
ctx.AbortWithError(404, errors.New("unknown plugin"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
instance, err := c.Manager.Instance(id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(404, errors.New("plugin instance not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if aborted := supportOrAbort(ctx, instance, compat.Configurer); aborted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newConf := instance.DefaultConfig()
|
||||||
|
newconfBytes, err := ioutil.ReadAll(ctx.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(newconfBytes, newConf); err != nil {
|
||||||
|
ctx.AbortWithError(400, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := instance.ValidateAndSetConfig(newConf); err != nil {
|
||||||
|
ctx.AbortWithError(400, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conf.Config = newconfBytes
|
||||||
|
c.DB.UpdatePluginConf(conf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPluginOwner(ctx *gin.Context, conf *model.PluginConf) bool {
|
||||||
|
return conf.UserID == auth.GetUserID(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportOrAbort(ctx *gin.Context, instance compat.PluginInstance, module compat.Capability) (aborted bool) {
|
||||||
|
if compat.HasSupport(instance, module) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ctx.AbortWithError(400, fmt.Errorf("plugin does not support %s", module))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,660 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-yaml/yaml"
|
||||||
|
|
||||||
|
"github.com/gotify/server/mode"
|
||||||
|
"github.com/gotify/server/model"
|
||||||
|
"github.com/gotify/server/plugin"
|
||||||
|
"github.com/gotify/server/plugin/compat"
|
||||||
|
"github.com/gotify/server/plugin/testing/mock"
|
||||||
|
"github.com/gotify/server/test"
|
||||||
|
"github.com/gotify/server/test/testdb"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPluginSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(PluginSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
db *testdb.Database
|
||||||
|
a *PluginAPI
|
||||||
|
ctx *gin.Context
|
||||||
|
recorder *httptest.ResponseRecorder
|
||||||
|
manager *plugin.Manager
|
||||||
|
notified bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) BeforeTest(suiteName, testName string) {
|
||||||
|
mode.Set(mode.TestDev)
|
||||||
|
rand.Seed(50)
|
||||||
|
s.db = testdb.NewDB(s.T())
|
||||||
|
s.resetRecorder()
|
||||||
|
manager, err := plugin.NewManager(s.db, "", nil, s)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
s.manager = manager
|
||||||
|
withURL(s.ctx, "http", "example.com")
|
||||||
|
s.a = &PluginAPI{DB: s.db, Manager: manager, Notifier: s}
|
||||||
|
|
||||||
|
mockPluginCompat := new(mock.Plugin)
|
||||||
|
assert.Nil(s.T(), s.manager.LoadPlugin(mockPluginCompat))
|
||||||
|
|
||||||
|
s.db.User(1)
|
||||||
|
assert.Nil(s.T(), s.manager.InitializeForUserID(1))
|
||||||
|
s.db.User(2)
|
||||||
|
assert.Nil(s.T(), s.manager.InitializeForUserID(2))
|
||||||
|
|
||||||
|
s.db.CreatePluginConf(&model.PluginConf{
|
||||||
|
UserID: 1,
|
||||||
|
ModulePath: "github.com/gotify/server/plugin/example/removed",
|
||||||
|
Token: "P1234",
|
||||||
|
Enabled: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) getDanglingConf(uid uint) *model.PluginConf {
|
||||||
|
return s.db.GetPluginConfByUserAndPath(uid, "github.com/gotify/server/plugin/example/removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) resetRecorder() {
|
||||||
|
s.recorder = httptest.NewRecorder()
|
||||||
|
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) AfterTest(suiteName, testName string) {
|
||||||
|
s.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Notify(userID uint, msg *model.MessageExternal) {
|
||||||
|
s.notified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_GetPlugins() {
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("GET", "/plugin", nil)
|
||||||
|
s.a.GetPlugins(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||||
|
|
||||||
|
pluginConfs := make([]model.PluginConfExternal, 0)
|
||||||
|
assert.Nil(s.T(), json.Unmarshal(s.recorder.Body.Bytes(), &pluginConfs))
|
||||||
|
|
||||||
|
assert.Equal(s.T(), mock.Name, pluginConfs[0].Name)
|
||||||
|
assert.Equal(s.T(), mock.ModulePath, pluginConfs[0].ModulePath)
|
||||||
|
|
||||||
|
assert.False(s.T(), pluginConfs[0].Enabled, "Plugins should be disabled by default")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_EnableDisablePlugin() {
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/plugin/1/enable", nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||||
|
s.a.EnablePlugin(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||||
|
|
||||||
|
assert.True(s.T(), s.db.GetPluginConfByUserAndPath(1, mock.ModulePath).Enabled)
|
||||||
|
s.resetRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/plugin/1/enable", nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||||
|
s.a.EnablePlugin(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||||
|
|
||||||
|
assert.True(s.T(), s.db.GetPluginConfByUserAndPath(1, mock.ModulePath).Enabled)
|
||||||
|
s.resetRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/plugin/1/disable", nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||||
|
s.a.DisablePlugin(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||||
|
|
||||||
|
assert.False(s.T(), s.db.GetPluginConfByUserAndPath(1, mock.ModulePath).Enabled)
|
||||||
|
s.resetRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/plugin/1/disable", nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||||
|
s.a.DisablePlugin(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||||
|
|
||||||
|
assert.False(s.T(), s.db.GetPluginConfByUserAndPath(1, mock.ModulePath).Enabled)
|
||||||
|
s.resetRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_EnableDisablePlugin_EnableReturnsError_expect500() {
|
||||||
|
s.db.User(16)
|
||||||
|
assert.Nil(s.T(), s.manager.InitializeForUserID(16))
|
||||||
|
mock.ReturnErrorOnEnableForUser(16, errors.New("test error"))
|
||||||
|
conf := s.db.GetPluginConfByUserAndPath(16, mock.ModulePath)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 16)
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", fmt.Sprintf("/plugin/%d/enable", conf.ID), nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.EnablePlugin(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 500, s.recorder.Code)
|
||||||
|
|
||||||
|
assert.False(s.T(), s.db.GetPluginConfByUserAndPath(1, mock.ModulePath).Enabled)
|
||||||
|
s.resetRecorder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_EnableDisablePlugin_DisableReturnsError_expect500() {
|
||||||
|
s.db.User(17)
|
||||||
|
assert.Nil(s.T(), s.manager.InitializeForUserID(17))
|
||||||
|
mock.ReturnErrorOnDisableForUser(17, errors.New("test error"))
|
||||||
|
conf := s.db.GetPluginConfByUserAndPath(17, mock.ModulePath)
|
||||||
|
s.manager.SetPluginEnabled(conf.ID, true)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 17)
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", fmt.Sprintf("/plugin/%d/disable", conf.ID), nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.DisablePlugin(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 500, s.recorder.Code)
|
||||||
|
|
||||||
|
assert.False(s.T(), s.db.GetPluginConfByUserAndPath(1, mock.ModulePath).Enabled)
|
||||||
|
s.resetRecorder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_EnableDisablePlugin_incorrectUser_expectNotFound() {
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 2)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/plugin/1/enable", nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||||
|
s.a.EnablePlugin(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
|
||||||
|
assert.False(s.T(), s.db.GetPluginConfByUserAndPath(1, mock.ModulePath).Enabled)
|
||||||
|
s.resetRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 2)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/plugin/1/disable", nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||||
|
s.a.DisablePlugin(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
|
||||||
|
assert.False(s.T(), s.db.GetPluginConfByUserAndPath(1, mock.ModulePath).Enabled)
|
||||||
|
s.resetRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_EnableDisablePlugin_nonExistPlugin_expectNotFound() {
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/plugin/99/enable", nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: "99"}}
|
||||||
|
s.a.EnablePlugin(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
s.resetRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/plugin/99/disable", nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: "99"}}
|
||||||
|
s.a.DisablePlugin(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
s.resetRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_EnableDisablePlugin_danglingConf_expectNotFound() {
|
||||||
|
conf := s.getDanglingConf(1)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", fmt.Sprintf("/plugin/%d/enable", conf.ID), nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.EnablePlugin(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
s.resetRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", fmt.Sprintf("/plugin/%d/disable", conf.ID), nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.DisablePlugin(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
s.resetRecorder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_GetDisplay() {
|
||||||
|
conf := s.db.GetPluginConfByUserAndPath(1, mock.ModulePath)
|
||||||
|
inst, err := s.manager.Instance(conf.ID)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
mockInst := inst.(*mock.PluginInstance)
|
||||||
|
|
||||||
|
mockInst.DisplayString = "test string"
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("GET", fmt.Sprintf("/plugin/%d/display", conf.ID), nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.GetDisplay(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||||
|
test.JSONEquals(s.T(), mockInst.DisplayString, s.recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_GetDisplay_NotImplemented_expectEmptyString() {
|
||||||
|
conf := s.db.GetPluginConfByUserAndPath(1, mock.ModulePath)
|
||||||
|
inst, err := s.manager.Instance(conf.ID)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
mockInst := inst.(*mock.PluginInstance)
|
||||||
|
|
||||||
|
mockInst.SetCapability(compat.Displayer, false)
|
||||||
|
defer mockInst.SetCapability(compat.Displayer, true)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("GET", fmt.Sprintf("/plugin/%d/display", conf.ID), nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.GetDisplay(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||||
|
test.JSONEquals(s.T(), "", s.recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_GetDisplay_incorrectUser_expectNotFound() {
|
||||||
|
conf := s.db.GetPluginConfByUserAndPath(1, mock.ModulePath)
|
||||||
|
inst, err := s.manager.Instance(conf.ID)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
mockInst := inst.(*mock.PluginInstance)
|
||||||
|
|
||||||
|
mockInst.DisplayString = "test string"
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 2)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("GET", fmt.Sprintf("/plugin/%d/display", conf.ID), nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.GetDisplay(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_GetDisplay_danglingConf_expectNotFound() {
|
||||||
|
conf := s.getDanglingConf(1)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("GET", fmt.Sprintf("/plugin/%d/display", conf.ID), nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.GetDisplay(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_GetDisplay_nonExistPlugin_expectNotFound() {
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("GET", "/plugin/99/display", nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: "99"}}
|
||||||
|
s.a.GetDisplay(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_GetConfig() {
|
||||||
|
conf := s.db.GetPluginConfByUserAndPath(1, mock.ModulePath)
|
||||||
|
inst, err := s.manager.Instance(conf.ID)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
mockInst := inst.(*mock.PluginInstance)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), mockInst.DefaultConfig(), mockInst.Config, "Initial config should be default config")
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("GET", fmt.Sprintf("/plugin/%d/config", conf.ID), nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.GetConfig(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||||
|
returnedConfig := new(mock.PluginConfig)
|
||||||
|
assert.Nil(s.T(), yaml.Unmarshal(s.recorder.Body.Bytes(), returnedConfig))
|
||||||
|
assert.Equal(s.T(), mockInst.Config, returnedConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_GetConfg_notImplemeted_expect400() {
|
||||||
|
conf := s.db.GetPluginConfByUserAndPath(1, mock.ModulePath)
|
||||||
|
inst, err := s.manager.Instance(conf.ID)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
mockInst := inst.(*mock.PluginInstance)
|
||||||
|
|
||||||
|
mockInst.SetCapability(compat.Configurer, false)
|
||||||
|
defer mockInst.SetCapability(compat.Configurer, true)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("GET", fmt.Sprintf("/plugin/%d/config", conf.ID), nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.GetConfig(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_GetConfig_incorrectUser_expectNotFound() {
|
||||||
|
conf := s.db.GetPluginConfByUserAndPath(1, mock.ModulePath)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 2)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("GET", fmt.Sprintf("/plugin/%d/config", conf.ID), nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.GetConfig(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_GetConfig_danglingConf_expectNotFound() {
|
||||||
|
conf := s.getDanglingConf(1)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("GET", fmt.Sprintf("/plugin/%d/config", conf.ID), nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.GetConfig(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_GetConfig_nonExistPlugin_expectNotFound() {
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("GET", "/plugin/99/config", nil)
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: "99"}}
|
||||||
|
s.a.GetConfig(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_UpdateConfig() {
|
||||||
|
conf := s.db.GetPluginConfByUserAndPath(1, mock.ModulePath)
|
||||||
|
inst, err := s.manager.Instance(conf.ID)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
mockInst := inst.(*mock.PluginInstance)
|
||||||
|
|
||||||
|
newConfig := &mock.PluginConfig{
|
||||||
|
TestKey: "test__new__config",
|
||||||
|
}
|
||||||
|
newConfigYAML, err := yaml.Marshal(newConfig)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", fmt.Sprintf("/plugin/%d/config", conf.ID), bytes.NewReader(newConfigYAML))
|
||||||
|
s.ctx.Header("Content-Type", "application/x-yaml")
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.UpdateConfig(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||||
|
assert.Equal(s.T(), newConfig, mockInst.Config, "config should be received by plugin")
|
||||||
|
|
||||||
|
pluginFromDBBytes := s.db.GetPluginConfByID(conf.ID).Config
|
||||||
|
pluginFromDB := new(mock.PluginConfig)
|
||||||
|
err := yaml.Unmarshal(pluginFromDBBytes, pluginFromDB)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), newConfig, pluginFromDB, "config should be updated in database")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_UpdateConfig_invalidConfig_expect400() {
|
||||||
|
conf := s.db.GetPluginConfByUserAndPath(1, mock.ModulePath)
|
||||||
|
inst, err := s.manager.Instance(conf.ID)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
mockInst := inst.(*mock.PluginInstance)
|
||||||
|
origConfig := mockInst.Config
|
||||||
|
|
||||||
|
newConfig := &mock.PluginConfig{
|
||||||
|
TestKey: "test__new__config__invalid",
|
||||||
|
IsNotValid: true,
|
||||||
|
}
|
||||||
|
newConfigYAML, err := yaml.Marshal(newConfig)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", fmt.Sprintf("/plugin/%d/config", conf.ID), bytes.NewReader(newConfigYAML))
|
||||||
|
s.ctx.Header("Content-Type", "application/x-yaml")
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.UpdateConfig(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||||
|
assert.Equal(s.T(), origConfig, mockInst.Config, "config should not be received by plugin")
|
||||||
|
|
||||||
|
pluginFromDBBytes := s.db.GetPluginConfByID(conf.ID).Config
|
||||||
|
pluginFromDB := new(mock.PluginConfig)
|
||||||
|
err := yaml.Unmarshal(pluginFromDBBytes, pluginFromDB)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), origConfig, pluginFromDB, "config should not be updated in database")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_UpdateConfig_malformedYAML_expect400() {
|
||||||
|
conf := s.db.GetPluginConfByUserAndPath(1, mock.ModulePath)
|
||||||
|
inst, err := s.manager.Instance(conf.ID)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
mockInst := inst.(*mock.PluginInstance)
|
||||||
|
origConfig := mockInst.Config
|
||||||
|
|
||||||
|
newConfigYAML := []byte(`--- "rg e""`)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", fmt.Sprintf("/plugin/%d/config", conf.ID), bytes.NewReader(newConfigYAML))
|
||||||
|
s.ctx.Header("Content-Type", "application/x-yaml")
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.UpdateConfig(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||||
|
assert.Equal(s.T(), origConfig, mockInst.Config, "config should not be received by plugin")
|
||||||
|
|
||||||
|
pluginFromDBBytes := s.db.GetPluginConfByID(conf.ID).Config
|
||||||
|
pluginFromDB := new(mock.PluginConfig)
|
||||||
|
err := yaml.Unmarshal(pluginFromDBBytes, pluginFromDB)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), origConfig, pluginFromDB, "config should not be updated in database")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_UpdateConfig_ioError_expect500() {
|
||||||
|
conf := s.db.GetPluginConfByUserAndPath(1, mock.ModulePath)
|
||||||
|
inst, err := s.manager.Instance(conf.ID)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
mockInst := inst.(*mock.PluginInstance)
|
||||||
|
origConfig := mockInst.Config
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", fmt.Sprintf("/plugin/%d/config", conf.ID), test.UnreadableReader())
|
||||||
|
s.ctx.Header("Content-Type", "application/x-yaml")
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.UpdateConfig(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 500, s.recorder.Code)
|
||||||
|
assert.Equal(s.T(), origConfig, mockInst.Config, "config should not be received by plugin")
|
||||||
|
|
||||||
|
pluginFromDBBytes := s.db.GetPluginConfByID(conf.ID).Config
|
||||||
|
pluginFromDB := new(mock.PluginConfig)
|
||||||
|
err := yaml.Unmarshal(pluginFromDBBytes, pluginFromDB)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), origConfig, pluginFromDB, "config should not be updated in database")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_UpdateConfig_notImplemented_expect400() {
|
||||||
|
conf := s.db.GetPluginConfByUserAndPath(1, mock.ModulePath)
|
||||||
|
inst, err := s.manager.Instance(conf.ID)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
mockInst := inst.(*mock.PluginInstance)
|
||||||
|
|
||||||
|
newConfig := &mock.PluginConfig{
|
||||||
|
TestKey: "test__new__config",
|
||||||
|
}
|
||||||
|
newConfigYAML, err := yaml.Marshal(newConfig)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
mockInst.SetCapability(compat.Configurer, false)
|
||||||
|
defer mockInst.SetCapability(compat.Configurer, true)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", fmt.Sprintf("/plugin/%d/config", conf.ID), bytes.NewReader(newConfigYAML))
|
||||||
|
s.ctx.Header("Content-Type", "application/x-yaml")
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.UpdateConfig(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_UpdateConfig_incorrectUser_expectNotFound() {
|
||||||
|
conf := s.db.GetPluginConfByUserAndPath(1, mock.ModulePath)
|
||||||
|
inst, err := s.manager.Instance(conf.ID)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
mockInst := inst.(*mock.PluginInstance)
|
||||||
|
origConfig := mockInst.Config
|
||||||
|
|
||||||
|
newConfig := &mock.PluginConfig{
|
||||||
|
TestKey: "test__new__config",
|
||||||
|
}
|
||||||
|
newConfigYAML, err := yaml.Marshal(newConfig)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 2)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", fmt.Sprintf("/plugin/%d/config", conf.ID), bytes.NewReader(newConfigYAML))
|
||||||
|
s.ctx.Header("Content-Type", "application/x-yaml")
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.UpdateConfig(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
assert.Equal(s.T(), origConfig, mockInst.Config, "config should not be received by plugin")
|
||||||
|
|
||||||
|
pluginFromDBBytes := s.db.GetPluginConfByID(conf.ID).Config
|
||||||
|
pluginFromDB := new(mock.PluginConfig)
|
||||||
|
err := yaml.Unmarshal(pluginFromDBBytes, pluginFromDB)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), origConfig, pluginFromDB, "config should not be updated in database")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_UpdateConfig_danglingConf_expectNotFound() {
|
||||||
|
conf := s.getDanglingConf(1)
|
||||||
|
|
||||||
|
newConfig := &mock.PluginConfig{
|
||||||
|
TestKey: "test__new__config",
|
||||||
|
}
|
||||||
|
newConfigYAML, err := yaml.Marshal(newConfig)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", fmt.Sprintf("/plugin/%d/config", conf.ID), bytes.NewReader(newConfigYAML))
|
||||||
|
s.ctx.Header("Content-Type", "application/x-yaml")
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprint(conf.ID)}}
|
||||||
|
s.a.UpdateConfig(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginSuite) Test_UpdateConfig_nonExistPlugin_expectNotFound() {
|
||||||
|
newConfig := &mock.PluginConfig{
|
||||||
|
TestKey: "test__new__config",
|
||||||
|
}
|
||||||
|
newConfigYAML, err := yaml.Marshal(newConfig)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
{
|
||||||
|
test.WithUser(s.ctx, 1)
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/plugin/99/config", bytes.NewReader(newConfigYAML))
|
||||||
|
s.ctx.Header("Content-Type", "application/x-yaml")
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: "99"}}
|
||||||
|
s.a.UpdateConfig(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,7 +37,7 @@ func New(pingPeriod, pongTimeout time.Duration, allowedWebSocketOrigins []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyDeletedUser closes existing connections for the given user.
|
// NotifyDeletedUser closes existing connections for the given user.
|
||||||
func (a *API) NotifyDeletedUser(userID uint) {
|
func (a *API) NotifyDeletedUser(userID uint) error {
|
||||||
a.lock.Lock()
|
a.lock.Lock()
|
||||||
defer a.lock.Unlock()
|
defer a.lock.Unlock()
|
||||||
if clients, ok := a.clients[userID]; ok {
|
if clients, ok := a.clients[userID]; ok {
|
||||||
|
|
@ -46,6 +46,7 @@ func (a *API) NotifyDeletedUser(userID uint) {
|
||||||
}
|
}
|
||||||
delete(a.clients, userID)
|
delete(a.clients, userID)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyDeletedClient closes existing connections with the given token.
|
// NotifyDeletedClient closes existing connections with the given token.
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ func TestWritePingFails(t *testing.T) {
|
||||||
|
|
||||||
assert.NotEmpty(t, clients)
|
assert.NotEmpty(t, clients)
|
||||||
|
|
||||||
time.Sleep(5 * time.Second) // waiting for ping
|
time.Sleep(api.pingPeriod) // waiting for ping
|
||||||
|
|
||||||
api.Notify(1, &model.MessageExternal{Message: "HI"})
|
api.Notify(1, &model.MessageExternal{Message: "HI"})
|
||||||
user.expectNoMessage()
|
user.expectNoMessage()
|
||||||
|
|
@ -123,7 +123,7 @@ func TestPing(t *testing.T) {
|
||||||
expectNoMessage(user)
|
expectNoMessage(user)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(2 * time.Second):
|
||||||
assert.Fail(t, "Expected ping but there was one :(")
|
assert.Fail(t, "Expected ping but there was one :(")
|
||||||
case <-ping:
|
case <-ping:
|
||||||
// expected
|
// expected
|
||||||
|
|
@ -151,7 +151,7 @@ func TestCloseClientOnNotReading(t *testing.T) {
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
assert.NotEmpty(t, clients(api, 1))
|
assert.NotEmpty(t, clients(api, 1))
|
||||||
|
|
||||||
time.Sleep(7 * time.Second)
|
time.Sleep(api.pingPeriod + api.pongTimeout)
|
||||||
|
|
||||||
assert.Empty(t, clients(api, 1))
|
assert.Empty(t, clients(api, 1))
|
||||||
}
|
}
|
||||||
|
|
@ -363,7 +363,7 @@ func TestMultipleClients(t *testing.T) {
|
||||||
expectNoMessage(userThree...)
|
expectNoMessage(userThree...)
|
||||||
|
|
||||||
api.Notify(1, &model.MessageExternal{ID: 1, Message: "hello"})
|
api.Notify(1, &model.MessageExternal{ID: 1, Message: "hello"})
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(500 * time.Millisecond)
|
||||||
expectMessage(&model.MessageExternal{ID: 1, Message: "hello"}, userOne...)
|
expectMessage(&model.MessageExternal{ID: 1, Message: "hello"}, userOne...)
|
||||||
expectNoMessage(userTwo...)
|
expectNoMessage(userTwo...)
|
||||||
expectNoMessage(userThree...)
|
expectNoMessage(userThree...)
|
||||||
|
|
@ -536,8 +536,8 @@ func (c *testingClient) expectNoMessage() {
|
||||||
func bootTestServer(handlerFunc gin.HandlerFunc) (*httptest.Server, *API) {
|
func bootTestServer(handlerFunc gin.HandlerFunc) (*httptest.Server, *API) {
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(handlerFunc)
|
r.Use(handlerFunc)
|
||||||
// all 4 seconds a ping, and the client has 1 second to respond
|
// ping every 500 ms, and the client has 500 ms to respond
|
||||||
api := New(4*time.Second, 1*time.Second, []string{})
|
api := New(500*time.Millisecond, 500*time.Millisecond, []string{})
|
||||||
|
|
||||||
r.GET("/", api.Handle)
|
r.GET("/", api.Handle)
|
||||||
server := httptest.NewServer(r)
|
server := httptest.NewServer(r)
|
||||||
|
|
|
||||||
49
api/user.go
49
api/user.go
|
|
@ -19,11 +19,45 @@ type UserDatabase interface {
|
||||||
CreateUser(user *model.User) error
|
CreateUser(user *model.User) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserChangeNotifier notifies listeners for user changes.
|
||||||
|
type UserChangeNotifier struct {
|
||||||
|
userDeletedCallbacks []func(uid uint) error
|
||||||
|
userAddedCallbacks []func(uid uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnUserDeleted is called on user deletion.
|
||||||
|
func (c *UserChangeNotifier) OnUserDeleted(cb func(uid uint) error) {
|
||||||
|
c.userDeletedCallbacks = append(c.userDeletedCallbacks, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnUserAdded is called on user creation.
|
||||||
|
func (c *UserChangeNotifier) OnUserAdded(cb func(uid uint) error) {
|
||||||
|
c.userAddedCallbacks = append(c.userAddedCallbacks, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserChangeNotifier) fireUserDeleted(uid uint) error {
|
||||||
|
for _, cb := range c.userDeletedCallbacks {
|
||||||
|
if err := cb(uid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserChangeNotifier) fireUserAdded(uid uint) error {
|
||||||
|
for _, cb := range c.userAddedCallbacks {
|
||||||
|
if err := cb(uid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// The UserAPI provides handlers for managing users.
|
// The UserAPI provides handlers for managing users.
|
||||||
type UserAPI struct {
|
type UserAPI struct {
|
||||||
DB UserDatabase
|
DB UserDatabase
|
||||||
PasswordStrength int
|
PasswordStrength int
|
||||||
NotifyDeleted func(uint)
|
UserChangeNotifier *UserChangeNotifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsers returns all the users
|
// GetUsers returns all the users
|
||||||
|
|
@ -125,6 +159,10 @@ func (a *UserAPI) CreateUser(ctx *gin.Context) {
|
||||||
internal := a.toInternalUser(&user, []byte{})
|
internal := a.toInternalUser(&user, []byte{})
|
||||||
if a.DB.GetUserByName(internal.Name) == nil {
|
if a.DB.GetUserByName(internal.Name) == nil {
|
||||||
a.DB.CreateUser(internal)
|
a.DB.CreateUser(internal)
|
||||||
|
if err := a.UserChangeNotifier.fireUserAdded(internal.ID); err != nil {
|
||||||
|
ctx.AbortWithError(500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.JSON(200, toExternalUser(internal))
|
ctx.JSON(200, toExternalUser(internal))
|
||||||
} else {
|
} else {
|
||||||
ctx.AbortWithError(400, errors.New("username already exists"))
|
ctx.AbortWithError(400, errors.New("username already exists"))
|
||||||
|
|
@ -214,7 +252,10 @@ func (a *UserAPI) GetUserByID(ctx *gin.Context) {
|
||||||
func (a *UserAPI) DeleteUserByID(ctx *gin.Context) {
|
func (a *UserAPI) DeleteUserByID(ctx *gin.Context) {
|
||||||
withID(ctx, "id", func(id uint) {
|
withID(ctx, "id", func(id uint) {
|
||||||
if user := a.DB.GetUserByID(id); user != nil {
|
if user := a.DB.GetUserByID(id); user != nil {
|
||||||
a.NotifyDeleted(id)
|
if err := a.UserChangeNotifier.fireUserDeleted(id); err != nil {
|
||||||
|
ctx.AbortWithError(500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
a.DB.DeleteUserByID(id)
|
a.DB.DeleteUserByID(id)
|
||||||
} else {
|
} else {
|
||||||
ctx.AbortWithError(404, errors.New("user does not exist"))
|
ctx.AbortWithError(404, errors.New("user does not exist"))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -10,6 +11,7 @@ import (
|
||||||
"github.com/gotify/server/mode"
|
"github.com/gotify/server/mode"
|
||||||
"github.com/gotify/server/model"
|
"github.com/gotify/server/model"
|
||||||
"github.com/gotify/server/test"
|
"github.com/gotify/server/test"
|
||||||
|
"github.com/gotify/server/test/testdb"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
@ -20,26 +22,31 @@ func TestUserSuite(t *testing.T) {
|
||||||
|
|
||||||
type UserSuite struct {
|
type UserSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
db *test.Database
|
db *testdb.Database
|
||||||
a *UserAPI
|
a *UserAPI
|
||||||
ctx *gin.Context
|
ctx *gin.Context
|
||||||
recorder *httptest.ResponseRecorder
|
recorder *httptest.ResponseRecorder
|
||||||
notified bool
|
notifiedAdd bool
|
||||||
|
notifiedDelete bool
|
||||||
|
notifier *UserChangeNotifier
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserSuite) BeforeTest(suiteName, testName string) {
|
func (s *UserSuite) BeforeTest(suiteName, testName string) {
|
||||||
mode.Set(mode.TestDev)
|
mode.Set(mode.TestDev)
|
||||||
s.recorder = httptest.NewRecorder()
|
s.recorder = httptest.NewRecorder()
|
||||||
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
||||||
s.db = test.NewDB(s.T())
|
s.db = testdb.NewDB(s.T())
|
||||||
s.notified = false
|
s.notifier = new(UserChangeNotifier)
|
||||||
s.a = &UserAPI{DB: s.db, NotifyDeleted: s.notify}
|
s.notifier.OnUserDeleted(func(uint) error {
|
||||||
|
s.notifiedDelete = true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
s.notifier.OnUserAdded(func(uint) error {
|
||||||
|
s.notifiedAdd = true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
s.a = &UserAPI{DB: s.db, UserChangeNotifier: s.notifier}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserSuite) notify(uint) {
|
|
||||||
s.notified = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *UserSuite) AfterTest(suiteName, testName string) {
|
func (s *UserSuite) AfterTest(suiteName, testName string) {
|
||||||
s.db.Close()
|
s.db.Close()
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +120,7 @@ func (s *UserSuite) Test_DeleteUserByID_UnknownUser() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserSuite) Test_DeleteUserByID() {
|
func (s *UserSuite) Test_DeleteUserByID() {
|
||||||
assert.False(s.T(), s.notified)
|
assert.False(s.T(), s.notifiedDelete)
|
||||||
|
|
||||||
s.db.User(2)
|
s.db.User(2)
|
||||||
|
|
||||||
|
|
@ -123,10 +130,27 @@ func (s *UserSuite) Test_DeleteUserByID() {
|
||||||
|
|
||||||
assert.Equal(s.T(), 200, s.recorder.Code)
|
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||||
s.db.AssertUserNotExist(2)
|
s.db.AssertUserNotExist(2)
|
||||||
assert.True(s.T(), s.notified)
|
assert.True(s.T(), s.notifiedDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserSuite) Test_DeleteUserByID_NotifyFail() {
|
||||||
|
s.db.User(5)
|
||||||
|
s.notifier.OnUserDeleted(func(id uint) error {
|
||||||
|
if id == 5 {
|
||||||
|
return errors.New("some error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
s.ctx.Params = gin.Params{{Key: "id", Value: "5"}}
|
||||||
|
|
||||||
|
s.a.DeleteUserByID(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 500, s.recorder.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserSuite) Test_CreateUser() {
|
func (s *UserSuite) Test_CreateUser() {
|
||||||
|
assert.False(s.T(), s.notifiedAdd)
|
||||||
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "mylittlepony", "admin": true}`))
|
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "mylittlepony", "admin": true}`))
|
||||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
|
@ -139,6 +163,22 @@ func (s *UserSuite) Test_CreateUser() {
|
||||||
created := s.db.GetUserByName("tom")
|
created := s.db.GetUserByName("tom")
|
||||||
assert.NotNil(s.T(), created)
|
assert.NotNil(s.T(), created)
|
||||||
assert.True(s.T(), password.ComparePassword(created.Pass, []byte("mylittlepony")))
|
assert.True(s.T(), password.ComparePassword(created.Pass, []byte("mylittlepony")))
|
||||||
|
assert.True(s.T(), s.notifiedAdd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserSuite) Test_CreateUser_NotifyFail() {
|
||||||
|
s.notifier.OnUserAdded(func(id uint) error {
|
||||||
|
if s.db.GetUserByID(id).Name == "eva" {
|
||||||
|
return errors.New("some error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "eva", "pass": "mylittlepony", "admin": true}`))
|
||||||
|
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
s.a.CreateUser(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 500, s.recorder.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserSuite) Test_CreateUser_NoPassword() {
|
func (s *UserSuite) Test_CreateUser_NoPassword() {
|
||||||
|
|
|
||||||
7
app.go
7
app.go
|
|
@ -33,7 +33,12 @@ func main() {
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
conf := config.Get()
|
conf := config.Get()
|
||||||
|
|
||||||
if err := os.MkdirAll(conf.UploadedImagesDir, 0777); err != nil {
|
if conf.PluginsDir != "" {
|
||||||
|
if err := os.MkdirAll(conf.PluginsDir, 0755); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(conf.UploadedImagesDir, 0755); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const (
|
||||||
type Database interface {
|
type Database interface {
|
||||||
GetApplicationByToken(token string) *model.Application
|
GetApplicationByToken(token string) *model.Application
|
||||||
GetClientByToken(token string) *model.Client
|
GetClientByToken(token string) *model.Client
|
||||||
|
GetPluginConfByToken(token string) *model.PluginConf
|
||||||
GetUserByName(name string) *model.User
|
GetUserByName(name string) *model.User
|
||||||
GetUserByID(id uint) *model.User
|
GetUserByID(id uint) *model.User
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/gotify/server/auth/password"
|
"github.com/gotify/server/auth/password"
|
||||||
"github.com/gotify/server/mode"
|
"github.com/gotify/server/mode"
|
||||||
"github.com/gotify/server/model"
|
"github.com/gotify/server/model"
|
||||||
"github.com/gotify/server/test"
|
"github.com/gotify/server/test/testdb"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
@ -23,12 +23,12 @@ func TestSuite(t *testing.T) {
|
||||||
type AuthenticationSuite struct {
|
type AuthenticationSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
auth *Auth
|
auth *Auth
|
||||||
DB *test.Database
|
DB *testdb.Database
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthenticationSuite) SetupSuite() {
|
func (s *AuthenticationSuite) SetupSuite() {
|
||||||
mode.Set(mode.TestDev)
|
mode.Set(mode.TestDev)
|
||||||
s.DB = test.NewDB(s.T())
|
s.DB = testdb.NewDB(s.T())
|
||||||
s.auth = &Auth{s.DB}
|
s.auth = &Auth{s.DB}
|
||||||
|
|
||||||
s.DB.CreateUser(&model.User{
|
s.DB.CreateUser(&model.User{
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,19 @@ var (
|
||||||
randomTokenLength = 14
|
randomTokenLength = 14
|
||||||
applicationPrefix = "A"
|
applicationPrefix = "A"
|
||||||
clientPrefix = "C"
|
clientPrefix = "C"
|
||||||
|
pluginPrefix = "P"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GenerateNotExistingToken receives a token generation func and a func to check whether the token exists, returns a unique token.
|
||||||
|
func GenerateNotExistingToken(generateToken func() string, tokenExists func(token string) bool) string {
|
||||||
|
for {
|
||||||
|
token := generateToken()
|
||||||
|
if !tokenExists(token) {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateApplicationToken generates an application token.
|
// GenerateApplicationToken generates an application token.
|
||||||
func GenerateApplicationToken() string {
|
func GenerateApplicationToken() string {
|
||||||
return generateRandomToken(applicationPrefix)
|
return generateRandomToken(applicationPrefix)
|
||||||
|
|
@ -21,6 +32,11 @@ func GenerateClientToken() string {
|
||||||
return generateRandomToken(clientPrefix)
|
return generateRandomToken(clientPrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GeneratePluginToken generates a plugin token.
|
||||||
|
func GeneratePluginToken() string {
|
||||||
|
return generateRandomToken(pluginPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateImageName generates an image name.
|
// GenerateImageName generates an image name.
|
||||||
func GenerateImageName() string {
|
func GenerateImageName() string {
|
||||||
return generateRandomString(25)
|
return generateRandomString(25)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -11,6 +12,21 @@ func TestTokenHavePrefix(t *testing.T) {
|
||||||
for i := 0; i < 50; i++ {
|
for i := 0; i < 50; i++ {
|
||||||
assert.True(t, strings.HasPrefix(GenerateApplicationToken(), "A"))
|
assert.True(t, strings.HasPrefix(GenerateApplicationToken(), "A"))
|
||||||
assert.True(t, strings.HasPrefix(GenerateClientToken(), "C"))
|
assert.True(t, strings.HasPrefix(GenerateClientToken(), "C"))
|
||||||
|
assert.True(t, strings.HasPrefix(GeneratePluginToken(), "P"))
|
||||||
assert.NotEmpty(t, GenerateImageName())
|
assert.NotEmpty(t, GenerateImageName())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGenerateNotExistingToken(t *testing.T) {
|
||||||
|
count := 5
|
||||||
|
token := GenerateNotExistingToken(func() string {
|
||||||
|
return fmt.Sprint(count)
|
||||||
|
}, func(token string) bool {
|
||||||
|
count--
|
||||||
|
if token == "0" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
assert.Equal(t, "0", token)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,3 +36,4 @@ defaultuser: # on database creation, gotify creates an admin user
|
||||||
pass: admin # the password of the default user
|
pass: admin # the password of the default user
|
||||||
passstrength: 10 # the bcrypt password strength (higher = better but also slower)
|
passstrength: 10 # the bcrypt password strength (higher = better but also slower)
|
||||||
uploadedimagesdir: data/images # the directory for storing uploaded images
|
uploadedimagesdir: data/images # the directory for storing uploaded images
|
||||||
|
pluginsdir: data/plugins # the directory where plugin resides
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ type Configuration struct {
|
||||||
}
|
}
|
||||||
PassStrength int `default:"10"`
|
PassStrength int `default:"10"`
|
||||||
UploadedImagesDir string `default:"data/images"`
|
UploadedImagesDir string `default:"data/images"`
|
||||||
|
PluginsDir string `default:"data/plugins"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the configuration extracted from env variables or config file.
|
// Get returns the configuration extracted from env variables or config file.
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ database:
|
||||||
defaultuser:
|
defaultuser:
|
||||||
name: nicories
|
name: nicories
|
||||||
pass: 12345
|
pass: 12345
|
||||||
|
pluginsdir: data/plugins
|
||||||
`)
|
`)
|
||||||
file.Close()
|
file.Close()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
@ -103,6 +104,7 @@ defaultuser:
|
||||||
assert.Equal(t, "*", conf.Server.ResponseHeaders["Access-Control-Allow-Origin"])
|
assert.Equal(t, "*", conf.Server.ResponseHeaders["Access-Control-Allow-Origin"])
|
||||||
assert.Equal(t, "GET,POST", conf.Server.ResponseHeaders["Access-Control-Allow-Methods"])
|
assert.Equal(t, "GET,POST", conf.Server.ResponseHeaders["Access-Control-Allow-Methods"])
|
||||||
assert.Equal(t, []string{".+.example.com", "otherdomain.com"}, conf.Server.Stream.AllowedOrigins)
|
assert.Equal(t, []string{".+.example.com", "otherdomain.com"}, conf.Server.Stream.AllowedOrigins)
|
||||||
|
assert.Equal(t, "data/plugins", conf.PluginsDir)
|
||||||
|
|
||||||
assert.Nil(t, os.Remove("config.yml"))
|
assert.Nil(t, os.Remove("config.yml"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import (
|
||||||
var mkdirAll = os.MkdirAll
|
var mkdirAll = os.MkdirAll
|
||||||
|
|
||||||
// New creates a new wrapper for the gorm database framework.
|
// New creates a new wrapper for the gorm database framework.
|
||||||
func New(dialect, connection, defaultUser, defaultPass string, strength int, createDefaultUser bool) (*GormDatabase, error) {
|
func New(dialect, connection, defaultUser, defaultPass string, strength int, createDefaultUserIfNotExist bool) (*GormDatabase, error) {
|
||||||
createDirectoryIfSqlite(dialect, connection)
|
createDirectoryIfSqlite(dialect, connection)
|
||||||
|
|
||||||
db, err := gorm.Open(dialect, connection)
|
db, err := gorm.Open(dialect, connection)
|
||||||
|
|
@ -35,12 +35,11 @@ func New(dialect, connection, defaultUser, defaultPass string, strength int, cre
|
||||||
db.DB().SetMaxOpenConns(1)
|
db.DB().SetMaxOpenConns(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !db.HasTable(new(model.User)) && !db.HasTable(new(model.Message)) &&
|
db.AutoMigrate(new(model.User), new(model.Application), new(model.Message), new(model.Client), new(model.PluginConf))
|
||||||
!db.HasTable(new(model.Client)) && !db.HasTable(new(model.Application)) {
|
userCount := 0
|
||||||
db.AutoMigrate(new(model.User), new(model.Application), new(model.Message), new(model.Client))
|
db.Find(new(model.User)).Count(&userCount)
|
||||||
if createDefaultUser {
|
if createDefaultUserIfNotExist && userCount == 0 {
|
||||||
db.Create(&model.User{Name: defaultUser, Pass: password.CreatePassword(defaultPass, strength), Admin: true})
|
db.Create(&model.User{Name: defaultUser, Pass: password.CreatePassword(defaultPass, strength), Admin: true})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &GormDatabase{DB: db}, nil
|
return &GormDatabase{DB: db}, nil
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gotify/server/test"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
@ -15,56 +17,56 @@ func TestDatabaseSuite(t *testing.T) {
|
||||||
|
|
||||||
type DatabaseSuite struct {
|
type DatabaseSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
db *GormDatabase
|
db *GormDatabase
|
||||||
|
tmpDir test.TmpDir
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DatabaseSuite) BeforeTest(suiteName, testName string) {
|
func (s *DatabaseSuite) BeforeTest(suiteName, testName string) {
|
||||||
db, err := New("sqlite3", "testdb.db", "defaultUser", "defaultPass", 5, true)
|
s.tmpDir = test.NewTmpDir("gotify_databasesuite")
|
||||||
|
db, err := New("sqlite3", s.tmpDir.Path("testdb.db"), "defaultUser", "defaultPass", 5, true)
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
s.db = db
|
s.db = db
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DatabaseSuite) AfterTest(suiteName, testName string) {
|
func (s *DatabaseSuite) AfterTest(suiteName, testName string) {
|
||||||
s.db.Close()
|
s.db.Close()
|
||||||
assert.Nil(s.T(), os.Remove("testdb.db"))
|
assert.Nil(s.T(), s.tmpDir.Clean())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInvalidDialect(t *testing.T) {
|
func TestInvalidDialect(t *testing.T) {
|
||||||
_, err := New("asdf", "testdb.db", "defaultUser", "defaultPass", 5, true)
|
tmpDir := test.NewTmpDir("gotify_testinvaliddialect")
|
||||||
assert.NotNil(t, err)
|
defer tmpDir.Clean()
|
||||||
|
_, err := New("asdf", tmpDir.Path("testdb.db"), "defaultUser", "defaultPass", 5, true)
|
||||||
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateSqliteFolder(t *testing.T) {
|
func TestCreateSqliteFolder(t *testing.T) {
|
||||||
// ensure path not exists
|
tmpDir := test.NewTmpDir("gotify_testcreatesqlitefolder")
|
||||||
os.RemoveAll("somepath")
|
defer tmpDir.Clean()
|
||||||
|
|
||||||
db, err := New("sqlite3", "somepath/testdb.db", "defaultUser", "defaultPass", 5, true)
|
db, err := New("sqlite3", tmpDir.Path("somepath/testdb.db"), "defaultUser", "defaultPass", 5, true)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.DirExists(t, "somepath")
|
assert.DirExists(t, tmpDir.Path("somepath"))
|
||||||
db.Close()
|
db.Close()
|
||||||
|
|
||||||
assert.Nil(t, os.RemoveAll("somepath"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWithAlreadyExistingSqliteFolder(t *testing.T) {
|
func TestWithAlreadyExistingSqliteFolder(t *testing.T) {
|
||||||
// ensure path not exists
|
tmpDir := test.NewTmpDir("gotify_testwithexistingfolder")
|
||||||
os.RemoveAll("somepath")
|
defer tmpDir.Clean()
|
||||||
os.MkdirAll("somepath", 0777)
|
|
||||||
|
|
||||||
db, err := New("sqlite3", "somepath/testdb.db", "defaultUser", "defaultPass", 5, true)
|
db, err := New("sqlite3", tmpDir.Path("somepath/testdb.db"), "defaultUser", "defaultPass", 5, true)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.DirExists(t, "somepath")
|
assert.DirExists(t, tmpDir.Path("somepath"))
|
||||||
db.Close()
|
db.Close()
|
||||||
|
|
||||||
assert.Nil(t, os.RemoveAll("somepath"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPanicsOnMkdirError(t *testing.T) {
|
func TestPanicsOnMkdirError(t *testing.T) {
|
||||||
os.RemoveAll("somepath")
|
tmpDir := test.NewTmpDir("gotify_testpanicsonmkdirerror")
|
||||||
|
defer tmpDir.Clean()
|
||||||
mkdirAll = func(path string, perm os.FileMode) error {
|
mkdirAll = func(path string, perm os.FileMode) error {
|
||||||
return errors.New("ERROR")
|
return errors.New("ERROR")
|
||||||
}
|
}
|
||||||
assert.Panics(t, func() {
|
assert.Panics(t, func() {
|
||||||
New("sqlite3", "somepath/test.db", "defaultUser", "defaultPass", 5, true)
|
New("sqlite3", tmpDir.Path("somepath/test.db"), "defaultUser", "defaultPass", 5, true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gotify/server/model"
|
||||||
|
"github.com/gotify/server/test"
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMigration(t *testing.T) {
|
||||||
|
suite.Run(t, &MigrationSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type MigrationSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
tmpDir test.TmpDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MigrationSuite) BeforeTest(suiteName, testName string) {
|
||||||
|
s.tmpDir = test.NewTmpDir("gotify_migrationsuite")
|
||||||
|
db, err := gorm.Open("sqlite3", s.tmpDir.Path("test_obsolete.db"))
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
assert.Nil(s.T(), db.CreateTable(new(model.User)).Error)
|
||||||
|
assert.Nil(s.T(), db.Create(&model.User{
|
||||||
|
Name: "test_user",
|
||||||
|
Admin: true,
|
||||||
|
}).Error)
|
||||||
|
|
||||||
|
// we should not be able to create applications by now
|
||||||
|
assert.False(s.T(), db.HasTable(new(model.Application)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MigrationSuite) AfterTest(suiteName, testName string) {
|
||||||
|
assert.Nil(s.T(), s.tmpDir.Clean())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MigrationSuite) TestMigration() {
|
||||||
|
db, err := New("sqlite3", s.tmpDir.Path("test_obsolete.db"), "admin", "admin", 6, true)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
assert.True(s.T(), db.DB.HasTable(new(model.Application)))
|
||||||
|
|
||||||
|
// a user already exist, not adding a new user
|
||||||
|
assert.Nil(s.T(), db.GetUserByName("admin"))
|
||||||
|
|
||||||
|
// the old user should persist
|
||||||
|
assert.Equal(s.T(), true, db.GetUserByName("test_user").Admin)
|
||||||
|
|
||||||
|
// we should be able to create applications
|
||||||
|
assert.Nil(s.T(), db.CreateApplication(&model.Application{
|
||||||
|
Token: "A1234",
|
||||||
|
UserID: db.GetUserByName("test_user").ID,
|
||||||
|
Description: "this is a test application",
|
||||||
|
Name: "test application",
|
||||||
|
}))
|
||||||
|
assert.Equal(s.T(), "test application", db.GetApplicationByToken("A1234").Name)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gotify/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetPluginConfByUser gets plugin configurations from a user
|
||||||
|
func (d *GormDatabase) GetPluginConfByUser(userid uint) []*model.PluginConf {
|
||||||
|
var plugins []*model.PluginConf
|
||||||
|
d.DB.Where("user_id = ?", userid).Find(&plugins)
|
||||||
|
return plugins
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPluginConfByUserAndPath gets plugin configuration by user and file name
|
||||||
|
func (d *GormDatabase) GetPluginConfByUserAndPath(userid uint, path string) *model.PluginConf {
|
||||||
|
plugin := new(model.PluginConf)
|
||||||
|
d.DB.Where("user_id = ? AND module_path = ?", userid, path).First(plugin)
|
||||||
|
if plugin.ModulePath == path {
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPluginConfByApplicationID gets plugin configuration by its internal appid.
|
||||||
|
func (d *GormDatabase) GetPluginConfByApplicationID(appid uint) *model.PluginConf {
|
||||||
|
plugin := new(model.PluginConf)
|
||||||
|
d.DB.Where("application_id = ?", appid).First(plugin)
|
||||||
|
if plugin.ApplicationID == appid {
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePluginConf creates a new plugin configuration
|
||||||
|
func (d *GormDatabase) CreatePluginConf(p *model.PluginConf) error {
|
||||||
|
return d.DB.Create(p).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPluginConfByToken gets plugin configuration by plugin token
|
||||||
|
func (d *GormDatabase) GetPluginConfByToken(token string) *model.PluginConf {
|
||||||
|
plugin := new(model.PluginConf)
|
||||||
|
d.DB.Where("token = ?", token).First(plugin)
|
||||||
|
if plugin.Token == token {
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPluginConfByID gets plugin configuration by plugin ID
|
||||||
|
func (d *GormDatabase) GetPluginConfByID(id uint) *model.PluginConf {
|
||||||
|
plugin := new(model.PluginConf)
|
||||||
|
d.DB.Where("id = ?", id).First(plugin)
|
||||||
|
if plugin.ID == id {
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePluginConf updates plugin configuration
|
||||||
|
func (d *GormDatabase) UpdatePluginConf(p *model.PluginConf) error {
|
||||||
|
return d.DB.Save(p).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePluginConfByID deletes a plugin configuration by its id.
|
||||||
|
func (d *GormDatabase) DeletePluginConfByID(id uint) error {
|
||||||
|
return d.DB.Where("id = ?", id).Delete(&model.PluginConf{}).Error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gotify/server/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *DatabaseSuite) TestPluginConf() {
|
||||||
|
plugin := model.PluginConf{
|
||||||
|
ModulePath: "github.com/gotify/example-plugin",
|
||||||
|
Token: "Pabc",
|
||||||
|
UserID: 1,
|
||||||
|
Enabled: true,
|
||||||
|
Config: nil,
|
||||||
|
ApplicationID: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Nil(s.T(), s.db.CreatePluginConf(&plugin))
|
||||||
|
|
||||||
|
assert.Equal(s.T(), uint(1), plugin.ID)
|
||||||
|
assert.Equal(s.T(), "Pabc", s.db.GetPluginConfByUserAndPath(1, "github.com/gotify/example-plugin").Token)
|
||||||
|
assert.Equal(s.T(), true, s.db.GetPluginConfByToken("Pabc").Enabled)
|
||||||
|
assert.Equal(s.T(), "Pabc", s.db.GetPluginConfByApplicationID(2).Token)
|
||||||
|
assert.Equal(s.T(), "github.com/gotify/example-plugin", s.db.GetPluginConfByID(1).ModulePath)
|
||||||
|
|
||||||
|
assert.Nil(s.T(), s.db.GetPluginConfByToken("Pnotexist"))
|
||||||
|
assert.Nil(s.T(), s.db.GetPluginConfByID(12))
|
||||||
|
assert.Nil(s.T(), s.db.GetPluginConfByUserAndPath(1, "not/exist"))
|
||||||
|
assert.Nil(s.T(), s.db.GetPluginConfByApplicationID(99))
|
||||||
|
|
||||||
|
assert.Len(s.T(), s.db.GetPluginConfByUser(1), 1)
|
||||||
|
assert.Len(s.T(), s.db.GetPluginConfByUser(0), 0)
|
||||||
|
|
||||||
|
testConf := `{"test_config_key":"hello"}`
|
||||||
|
plugin.Enabled = false
|
||||||
|
plugin.Config = []byte(testConf)
|
||||||
|
assert.Nil(s.T(), s.db.UpdatePluginConf(&plugin))
|
||||||
|
assert.Equal(s.T(), false, s.db.GetPluginConfByToken("Pabc").Enabled)
|
||||||
|
assert.Equal(s.T(), testConf, string(s.db.GetPluginConfByToken("Pabc").Config))
|
||||||
|
}
|
||||||
|
|
@ -39,6 +39,9 @@ func (d *GormDatabase) DeleteUserByID(id uint) error {
|
||||||
for _, client := range d.GetClientsByUser(id) {
|
for _, client := range d.GetClientsByUser(id) {
|
||||||
d.DeleteClientByID(client.ID)
|
d.DeleteClientByID(client.ID)
|
||||||
}
|
}
|
||||||
|
for _, conf := range d.GetPluginConfByUser(id) {
|
||||||
|
d.DeletePluginConfByID(conf.ID)
|
||||||
|
}
|
||||||
return d.DB.Where("id = ?", id).Delete(&model.User{}).Error
|
return d.DB.Where("id = ?", id).Delete(&model.User{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,16 +46,38 @@ func (s *DatabaseSuite) TestUser() {
|
||||||
assert.Empty(s.T(), users)
|
assert.Empty(s.T(), users)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DatabaseSuite) TestDeleteUserDeletesApplicationsAndClients() {
|
func (s *DatabaseSuite) TestUserPlugins() {
|
||||||
|
s.db.CreateUser(&model.User{Name: "geek", ID: 16})
|
||||||
|
s.db.CreatePluginConf(&model.PluginConf{
|
||||||
|
UserID: s.db.GetUserByName("geek").ID,
|
||||||
|
ModulePath: "github.com/gotify/example-plugin",
|
||||||
|
Token: "P1234",
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
s.db.CreatePluginConf(&model.PluginConf{
|
||||||
|
UserID: s.db.GetUserByName("geek").ID,
|
||||||
|
ModulePath: "github.com/gotify/example-plugin/v2",
|
||||||
|
Token: "P5678",
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Len(s.T(), s.db.GetPluginConfByUser(s.db.GetUserByName("geek").ID), 2)
|
||||||
|
assert.Equal(s.T(), "github.com/gotify/example-plugin", s.db.GetPluginConfByToken("P1234").ModulePath)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DatabaseSuite) TestDeleteUserDeletesApplicationsAndClientsAndPluginConfs() {
|
||||||
s.db.CreateUser(&model.User{Name: "nicories", ID: 10})
|
s.db.CreateUser(&model.User{Name: "nicories", ID: 10})
|
||||||
s.db.CreateApplication(&model.Application{ID: 100, Token: "apptoken", UserID: 10})
|
s.db.CreateApplication(&model.Application{ID: 100, Token: "apptoken", UserID: 10})
|
||||||
s.db.CreateMessage(&model.Message{ID: 1000, ApplicationID: 100})
|
s.db.CreateMessage(&model.Message{ID: 1000, ApplicationID: 100})
|
||||||
s.db.CreateClient(&model.Client{ID: 10000, Token: "clienttoken", UserID: 10})
|
s.db.CreateClient(&model.Client{ID: 10000, Token: "clienttoken", UserID: 10})
|
||||||
|
s.db.CreatePluginConf(&model.PluginConf{ID: 1000, Token: "plugintoken", UserID: 10})
|
||||||
|
|
||||||
s.db.CreateUser(&model.User{Name: "nicories2", ID: 20})
|
s.db.CreateUser(&model.User{Name: "nicories2", ID: 20})
|
||||||
s.db.CreateApplication(&model.Application{ID: 200, Token: "apptoken2", UserID: 20})
|
s.db.CreateApplication(&model.Application{ID: 200, Token: "apptoken2", UserID: 20})
|
||||||
s.db.CreateMessage(&model.Message{ID: 2000, ApplicationID: 200})
|
s.db.CreateMessage(&model.Message{ID: 2000, ApplicationID: 200})
|
||||||
s.db.CreateClient(&model.Client{ID: 20000, Token: "clienttoken2", UserID: 20})
|
s.db.CreateClient(&model.Client{ID: 20000, Token: "clienttoken2", UserID: 20})
|
||||||
|
s.db.CreatePluginConf(&model.PluginConf{ID: 2000, Token: "plugintoken2", UserID: 20})
|
||||||
|
|
||||||
s.db.DeleteUserByID(10)
|
s.db.DeleteUserByID(10)
|
||||||
|
|
||||||
|
|
@ -65,12 +87,14 @@ func (s *DatabaseSuite) TestDeleteUserDeletesApplicationsAndClients() {
|
||||||
assert.Empty(s.T(), s.db.GetApplicationsByUser(10))
|
assert.Empty(s.T(), s.db.GetApplicationsByUser(10))
|
||||||
assert.Empty(s.T(), s.db.GetMessagesByApplication(100))
|
assert.Empty(s.T(), s.db.GetMessagesByApplication(100))
|
||||||
assert.Empty(s.T(), s.db.GetMessagesByUser(10))
|
assert.Empty(s.T(), s.db.GetMessagesByUser(10))
|
||||||
|
assert.Empty(s.T(), s.db.GetPluginConfByUser(10))
|
||||||
assert.Nil(s.T(), s.db.GetMessageByID(1000))
|
assert.Nil(s.T(), s.db.GetMessageByID(1000))
|
||||||
|
|
||||||
assert.NotNil(s.T(), s.db.GetApplicationByToken("apptoken2"))
|
assert.NotNil(s.T(), s.db.GetApplicationByToken("apptoken2"))
|
||||||
assert.NotNil(s.T(), s.db.GetClientByToken("clienttoken2"))
|
assert.NotNil(s.T(), s.db.GetClientByToken("clienttoken2"))
|
||||||
assert.NotEmpty(s.T(), s.db.GetClientsByUser(20))
|
assert.NotEmpty(s.T(), s.db.GetClientsByUser(20))
|
||||||
assert.NotEmpty(s.T(), s.db.GetApplicationsByUser(20))
|
assert.NotEmpty(s.T(), s.db.GetApplicationsByUser(20))
|
||||||
|
assert.NotEmpty(s.T(), s.db.GetPluginConfByUser(20))
|
||||||
assert.NotEmpty(s.T(), s.db.GetMessagesByApplication(200))
|
assert.NotEmpty(s.T(), s.db.GetMessagesByApplication(200))
|
||||||
assert.NotEmpty(s.T(), s.db.GetMessagesByUser(20))
|
assert.NotEmpty(s.T(), s.db.GetMessagesByUser(20))
|
||||||
assert.NotNil(s.T(), s.db.GetMessageByID(2000))
|
assert.NotNil(s.T(), s.db.GetMessageByID(2000))
|
||||||
|
|
|
||||||
490
docs/spec.json
490
docs/spec.json
|
|
@ -990,6 +990,404 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/plugin": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"clientTokenHeader": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientTokenQuery": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"basicAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"plugin"
|
||||||
|
],
|
||||||
|
"summary": "Return all plugins.",
|
||||||
|
"operationId": "getPlugins",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Ok",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/PluginConf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/plugin/:id/config": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"clientTokenHeader": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientTokenQuery": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"basicAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/x-yaml"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"plugin"
|
||||||
|
],
|
||||||
|
"summary": "Get YAML configuration for Configurer plugin.",
|
||||||
|
"operationId": "getPluginConfig",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "the plugin id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Ok",
|
||||||
|
"schema": {
|
||||||
|
"description": "plugin configuration",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"clientTokenHeader": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientTokenQuery": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"basicAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/x-yaml"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"plugin"
|
||||||
|
],
|
||||||
|
"summary": "Update YAML configuration for Configurer plugin.",
|
||||||
|
"operationId": "updatePluginConfig",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "the plugin id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Ok"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/plugin/:id/disable": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"clientTokenHeader": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientTokenQuery": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"basicAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"plugin"
|
||||||
|
],
|
||||||
|
"summary": "Disable a plugin.",
|
||||||
|
"operationId": "disablePlugin",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "the plugin id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Ok"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/plugin/:id/display": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"clientTokenHeader": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientTokenQuery": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"basicAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"plugin"
|
||||||
|
],
|
||||||
|
"summary": "Get display info for a Displayer plugin.",
|
||||||
|
"operationId": "getPluginDisplay",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "the plugin id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Ok",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/plugin/:id/enable": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"clientTokenHeader": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientTokenQuery": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"basicAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"plugin"
|
||||||
|
],
|
||||||
|
"summary": "Enable a plugin.",
|
||||||
|
"operationId": "enablePlugin",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "the plugin id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Ok"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/stream": {
|
"/stream": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -1383,6 +1781,7 @@
|
||||||
"token",
|
"token",
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
|
"internal",
|
||||||
"image"
|
"image"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -1407,6 +1806,13 @@
|
||||||
"readOnly": true,
|
"readOnly": true,
|
||||||
"example": "https://example.com/image.jpeg"
|
"example": "https://example.com/image.jpeg"
|
||||||
},
|
},
|
||||||
|
"internal": {
|
||||||
|
"description": "Whether the application is an internal application. Internal applications should not be deleted.",
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "Internal",
|
||||||
|
"readOnly": true,
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"description": "The application name. This is how the application should be displayed to the user.",
|
"description": "The application name. This is how the application should be displayed to the user.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
@ -1634,6 +2040,90 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "github.com/gotify/server/model"
|
"x-go-package": "github.com/gotify/server/model"
|
||||||
},
|
},
|
||||||
|
"PluginConf": {
|
||||||
|
"description": "Holds information about a plugin instance for one user.",
|
||||||
|
"type": "object",
|
||||||
|
"title": "PluginConfExternal Model",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"token",
|
||||||
|
"modulePath",
|
||||||
|
"enabled",
|
||||||
|
"capabilities"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"author": {
|
||||||
|
"description": "The author of the plugin.",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Author",
|
||||||
|
"readOnly": true,
|
||||||
|
"example": "jmattheis"
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"description": "Capabilities the plugin provides",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"x-go-name": "Capabilities",
|
||||||
|
"example": [
|
||||||
|
"webhook",
|
||||||
|
"display"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"description": "Whether the plugin instance is enabled.",
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "Enabled",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "The plugin id.",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint64",
|
||||||
|
"x-go-name": "ID",
|
||||||
|
"readOnly": true,
|
||||||
|
"example": 25
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"description": "The license of the plugin.",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "License",
|
||||||
|
"readOnly": true,
|
||||||
|
"example": "MIT"
|
||||||
|
},
|
||||||
|
"modulePath": {
|
||||||
|
"description": "The module path of the plugin.",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "ModulePath",
|
||||||
|
"readOnly": true,
|
||||||
|
"example": "github.com/gotify/server/plugin/example/echo"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The plugin name.",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Name",
|
||||||
|
"readOnly": true,
|
||||||
|
"example": "RSS poller"
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"description": "The user name. For login.",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Token",
|
||||||
|
"example": "P1234"
|
||||||
|
},
|
||||||
|
"website": {
|
||||||
|
"description": "The website of the plugin.",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Website",
|
||||||
|
"readOnly": true,
|
||||||
|
"example": "gotify.net"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-name": "PluginConfExternal",
|
||||||
|
"x-go-package": "github.com/gotify/server/model"
|
||||||
|
},
|
||||||
"User": {
|
"User": {
|
||||||
"description": "The User holds information about permission and other stuff.",
|
"description": "The User holds information about permission and other stuff.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ func TestValidationError(t *testing.T) {
|
||||||
ctx, _ := gin.CreateTestContext(rec)
|
ctx, _ := gin.CreateTestContext(rec)
|
||||||
ctx.Request = httptest.NewRequest("GET", "/uri", nil)
|
ctx.Request = httptest.NewRequest("GET", "/uri", nil)
|
||||||
|
|
||||||
assert.NotNil(t, ctx.Bind(&testValidate{Age: 150, Limit: 20}))
|
assert.Error(t, ctx.Bind(&testValidate{Age: 150, Limit: 20}))
|
||||||
Handler()(ctx)
|
Handler()(ctx)
|
||||||
|
|
||||||
err := new(model.Error)
|
err := new(model.Error)
|
||||||
|
|
|
||||||
22
go.mod
22
go.mod
|
|
@ -1,34 +1,28 @@
|
||||||
module github.com/gotify/server
|
module github.com/gotify/server
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.34.0 // indirect
|
cloud.google.com/go v0.35.1 // indirect
|
||||||
github.com/Southclaws/configor v1.0.0 // indirect
|
github.com/Southclaws/configor v1.0.0 // indirect
|
||||||
github.com/denisenkom/go-mssqldb v0.0.0-20190111225525-2fea367d496d // indirect
|
github.com/denisenkom/go-mssqldb v0.0.0-20190121005146-b04fd42d9952 // indirect
|
||||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
|
||||||
github.com/fortytw2/leaktest v1.3.0
|
github.com/fortytw2/leaktest v1.3.0
|
||||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect
|
|
||||||
github.com/gin-gonic/gin v1.3.0
|
github.com/gin-gonic/gin v1.3.0
|
||||||
github.com/go-sql-driver/mysql v1.4.1 // indirect
|
github.com/go-sql-driver/mysql v1.4.1 // indirect
|
||||||
github.com/gobuffalo/packr v1.21.9
|
github.com/go-yaml/yaml v2.1.0+incompatible
|
||||||
github.com/google/go-cmp v0.2.0 // indirect
|
github.com/gobuffalo/packr v1.22.0
|
||||||
github.com/gorilla/websocket v1.4.0
|
github.com/gorilla/websocket v1.4.0
|
||||||
github.com/gotify/configor v1.0.2-0.20190112111140-7d9c7c7e6233
|
github.com/gotify/configor v1.0.2-0.20190112111140-7d9c7c7e6233
|
||||||
github.com/gotify/location v0.0.0-20170722210143-03bc4ad20437
|
github.com/gotify/location v0.0.0-20170722210143-03bc4ad20437
|
||||||
github.com/h2non/filetype v1.0.5
|
github.com/gotify/plugin-api v1.0.0
|
||||||
|
github.com/h2non/filetype v1.0.6
|
||||||
github.com/jinzhu/gorm v1.9.2
|
github.com/jinzhu/gorm v1.9.2
|
||||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect
|
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect
|
||||||
github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3 // indirect
|
github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3 // indirect
|
||||||
github.com/json-iterator/go v1.1.5 // indirect
|
|
||||||
github.com/lib/pq v1.0.0 // indirect
|
github.com/lib/pq v1.0.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.4 // indirect
|
|
||||||
github.com/mattn/go-sqlite3 v1.10.0 // indirect
|
github.com/mattn/go-sqlite3 v1.10.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
|
||||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
|
||||||
github.com/pkg/errors v0.8.1 // indirect
|
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/stretchr/testify v1.3.0
|
||||||
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc
|
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613
|
||||||
google.golang.org/appengine v1.4.0 // indirect
|
google.golang.org/appengine v1.4.0 // indirect
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
|
||||||
gopkg.in/go-playground/validator.v8 v8.18.2
|
gopkg.in/go-playground/validator.v8 v8.18.2
|
||||||
gopkg.in/h2non/filetype.v1 v1.0.5 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
175
go.sum
175
go.sum
|
|
@ -1,5 +1,13 @@
|
||||||
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.35.1 h1:LMe/Btq0Eijsc97JyBwMc0KMXOe0orqAMdg7/EkywN8=
|
||||||
|
cloud.google.com/go v0.35.1/go.mod h1:wfjPZNvXCBYESy3fIynybskMP48KVPrjSPCnXiK7Prg=
|
||||||
|
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||||
|
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||||
|
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||||
|
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||||
|
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||||
|
github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY=
|
||||||
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
|
@ -7,18 +15,23 @@ github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0
|
||||||
github.com/Southclaws/configor v1.0.0 h1:0bt6XsYs0q3GlK1gOqdjoM4VJj9ePdd/GcNNjzH567A=
|
github.com/Southclaws/configor v1.0.0 h1:0bt6XsYs0q3GlK1gOqdjoM4VJj9ePdd/GcNNjzH567A=
|
||||||
github.com/Southclaws/configor v1.0.0/go.mod h1:LVoYKxkifbFIINnnXwmqeiH4ciRalQNDMwQETyFomTs=
|
github.com/Southclaws/configor v1.0.0/go.mod h1:LVoYKxkifbFIINnnXwmqeiH4ciRalQNDMwQETyFomTs=
|
||||||
github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
|
github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
|
||||||
github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0=
|
github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0=
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/denisenkom/go-mssqldb v0.0.0-20190111225525-2fea367d496d h1:M0bjbJ5PZPl4iKkt0FSvhfSCJI9NisDDda29jXN9i0c=
|
github.com/denisenkom/go-mssqldb v0.0.0-20190121005146-b04fd42d9952 h1:b5OnbZD49x9g+/FcYbs/vukEt8C/jUbGhCJ3uduQmu8=
|
||||||
github.com/denisenkom/go-mssqldb v0.0.0-20190111225525-2fea367d496d/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
|
github.com/denisenkom/go-mssqldb v0.0.0-20190121005146-b04fd42d9952/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
|
@ -27,13 +40,16 @@ github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY=
|
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY=
|
||||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||||
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
|
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
|
||||||
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
||||||
|
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
|
|
@ -52,6 +68,9 @@ github.com/gobuffalo/buffalo-plugins v1.6.9/go.mod h1:yYlYTrPdMCz+6/+UaXg5Jm4gN3
|
||||||
github.com/gobuffalo/buffalo-plugins v1.6.11/go.mod h1:eAA6xJIL8OuynJZ8amXjRmHND6YiusVAaJdHDN1Lu8Q=
|
github.com/gobuffalo/buffalo-plugins v1.6.11/go.mod h1:eAA6xJIL8OuynJZ8amXjRmHND6YiusVAaJdHDN1Lu8Q=
|
||||||
github.com/gobuffalo/buffalo-plugins v1.8.2/go.mod h1:9te6/VjEQ7pKp7lXlDIMqzxgGpjlKoAcAANdCgoR960=
|
github.com/gobuffalo/buffalo-plugins v1.8.2/go.mod h1:9te6/VjEQ7pKp7lXlDIMqzxgGpjlKoAcAANdCgoR960=
|
||||||
github.com/gobuffalo/buffalo-plugins v1.8.3/go.mod h1:IAWq6vjZJVXebIq2qGTLOdlXzmpyTZ5iJG5b59fza5U=
|
github.com/gobuffalo/buffalo-plugins v1.8.3/go.mod h1:IAWq6vjZJVXebIq2qGTLOdlXzmpyTZ5iJG5b59fza5U=
|
||||||
|
github.com/gobuffalo/buffalo-plugins v1.9.4/go.mod h1:grCV6DGsQlVzQwk6XdgcL3ZPgLm9BVxlBmXPMF8oBHI=
|
||||||
|
github.com/gobuffalo/buffalo-plugins v1.10.0/go.mod h1:4osg8d9s60txLuGwXnqH+RCjPHj9K466cDFRl3PErHI=
|
||||||
|
github.com/gobuffalo/buffalo-plugins v1.11.0/go.mod h1:rtIvAYRjYibgmWhnjKmo7OadtnxuMG5ZQLr25ozAzjg=
|
||||||
github.com/gobuffalo/buffalo-pop v1.0.5/go.mod h1:Fw/LfFDnSmB/vvQXPvcXEjzP98Tc+AudyNWUBWKCwQ8=
|
github.com/gobuffalo/buffalo-pop v1.0.5/go.mod h1:Fw/LfFDnSmB/vvQXPvcXEjzP98Tc+AudyNWUBWKCwQ8=
|
||||||
github.com/gobuffalo/envy v1.6.4/go.mod h1:Abh+Jfw475/NWtYMEt+hnJWRiC8INKWibIMyNt1w2Mc=
|
github.com/gobuffalo/envy v1.6.4/go.mod h1:Abh+Jfw475/NWtYMEt+hnJWRiC8INKWibIMyNt1w2Mc=
|
||||||
github.com/gobuffalo/envy v1.6.5/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
|
github.com/gobuffalo/envy v1.6.5/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
|
||||||
|
|
@ -60,8 +79,9 @@ github.com/gobuffalo/envy v1.6.7/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9k
|
||||||
github.com/gobuffalo/envy v1.6.8/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
|
github.com/gobuffalo/envy v1.6.8/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
|
||||||
github.com/gobuffalo/envy v1.6.9/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
|
github.com/gobuffalo/envy v1.6.9/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
|
||||||
github.com/gobuffalo/envy v1.6.10/go.mod h1:X0CFllQjTV5ogsnUrg+Oks2yTI+PU2dGYBJOEI2D1Uo=
|
github.com/gobuffalo/envy v1.6.10/go.mod h1:X0CFllQjTV5ogsnUrg+Oks2yTI+PU2dGYBJOEI2D1Uo=
|
||||||
github.com/gobuffalo/envy v1.6.11 h1:dCKSFypLRvqaaUtyzkfKKF2j35ce5agsqfyIrRmm02E=
|
|
||||||
github.com/gobuffalo/envy v1.6.11/go.mod h1:Fiq52W7nrHGDggFPhn2ZCcHw4u/rqXkqo+i7FB6EAcg=
|
github.com/gobuffalo/envy v1.6.11/go.mod h1:Fiq52W7nrHGDggFPhn2ZCcHw4u/rqXkqo+i7FB6EAcg=
|
||||||
|
github.com/gobuffalo/envy v1.6.12 h1:zkhss8DXz/pty2HAyA8BnvWMTYxo4gjd4+WCnYovoxY=
|
||||||
|
github.com/gobuffalo/envy v1.6.12/go.mod h1:qJNrJhKkZpEW0glh5xP2syQHH5kgdmgsKss2Kk8PTP0=
|
||||||
github.com/gobuffalo/events v1.0.3/go.mod h1:Txo8WmqScapa7zimEQIwgiJBvMECMe9gJjsKNPN3uZw=
|
github.com/gobuffalo/events v1.0.3/go.mod h1:Txo8WmqScapa7zimEQIwgiJBvMECMe9gJjsKNPN3uZw=
|
||||||
github.com/gobuffalo/events v1.0.7/go.mod h1:z8txf6H9jWhQ5Scr7YPLWg/cgXBRj8Q4uYI+rsVCCSQ=
|
github.com/gobuffalo/events v1.0.7/go.mod h1:z8txf6H9jWhQ5Scr7YPLWg/cgXBRj8Q4uYI+rsVCCSQ=
|
||||||
github.com/gobuffalo/events v1.0.8/go.mod h1:A5KyqT1sA+3GJiBE4QKZibse9mtOcI9nw8gGrDdqYGs=
|
github.com/gobuffalo/events v1.0.8/go.mod h1:A5KyqT1sA+3GJiBE4QKZibse9mtOcI9nw8gGrDdqYGs=
|
||||||
|
|
@ -70,6 +90,7 @@ github.com/gobuffalo/events v1.1.4/go.mod h1:09/YRRgZHEOts5Isov+g9X2xajxdvOAcUuA
|
||||||
github.com/gobuffalo/events v1.1.5/go.mod h1:3YUSzgHfYctSjEjLCWbkXP6djH2M+MLaVRzb4ymbAK0=
|
github.com/gobuffalo/events v1.1.5/go.mod h1:3YUSzgHfYctSjEjLCWbkXP6djH2M+MLaVRzb4ymbAK0=
|
||||||
github.com/gobuffalo/events v1.1.7/go.mod h1:6fGqxH2ing5XMb3EYRq9LEkVlyPGs4oO/eLzh+S8CxY=
|
github.com/gobuffalo/events v1.1.7/go.mod h1:6fGqxH2ing5XMb3EYRq9LEkVlyPGs4oO/eLzh+S8CxY=
|
||||||
github.com/gobuffalo/events v1.1.8/go.mod h1:UFy+W6X6VbCWS8k2iT81HYX65dMtiuVycMy04cplt/8=
|
github.com/gobuffalo/events v1.1.8/go.mod h1:UFy+W6X6VbCWS8k2iT81HYX65dMtiuVycMy04cplt/8=
|
||||||
|
github.com/gobuffalo/events v1.1.9/go.mod h1:/0nf8lMtP5TkgNbzYxR6Bl4GzBy5s5TebgNTdRfRbPM=
|
||||||
github.com/gobuffalo/fizz v1.0.12/go.mod h1:C0sltPxpYK8Ftvf64kbsQa2yiCZY4RZviurNxXdAKwc=
|
github.com/gobuffalo/fizz v1.0.12/go.mod h1:C0sltPxpYK8Ftvf64kbsQa2yiCZY4RZviurNxXdAKwc=
|
||||||
github.com/gobuffalo/flect v0.0.0-20180907193754-dc14d8acaf9f/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
github.com/gobuffalo/flect v0.0.0-20180907193754-dc14d8acaf9f/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
||||||
github.com/gobuffalo/flect v0.0.0-20181002182613-4571df4b1daf/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
github.com/gobuffalo/flect v0.0.0-20181002182613-4571df4b1daf/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
||||||
|
|
@ -79,6 +100,9 @@ github.com/gobuffalo/flect v0.0.0-20181019110701-3d6f0b585514/go.mod h1:rCiQgmAE
|
||||||
github.com/gobuffalo/flect v0.0.0-20181024204909-8f6be1a8c6c2/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
github.com/gobuffalo/flect v0.0.0-20181024204909-8f6be1a8c6c2/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
||||||
github.com/gobuffalo/flect v0.0.0-20181104133451-1f6e9779237a/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
github.com/gobuffalo/flect v0.0.0-20181104133451-1f6e9779237a/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
||||||
github.com/gobuffalo/flect v0.0.0-20181114183036-47375f6d8328/go.mod h1:0HvNbHdfh+WOvDSIASqJOSxTOWSxCCUF++k/Y53v9rI=
|
github.com/gobuffalo/flect v0.0.0-20181114183036-47375f6d8328/go.mod h1:0HvNbHdfh+WOvDSIASqJOSxTOWSxCCUF++k/Y53v9rI=
|
||||||
|
github.com/gobuffalo/flect v0.0.0-20181210151238-24a2b68e0316/go.mod h1:en58vff74S9b99Eg42Dr+/9yPu437QjlNsO/hBYPuOk=
|
||||||
|
github.com/gobuffalo/flect v0.0.0-20190104192022-4af577e09bf2/go.mod h1:en58vff74S9b99Eg42Dr+/9yPu437QjlNsO/hBYPuOk=
|
||||||
|
github.com/gobuffalo/flect v0.0.0-20190117212819-a62e61d96794/go.mod h1:397QT6v05LkZkn07oJXXT6y9FCfwC8Pug0WA2/2mE9k=
|
||||||
github.com/gobuffalo/genny v0.0.0-20180924032338-7af3a40f2252/go.mod h1:tUTQOogrr7tAQnhajMSH6rv1BVev34H2sa1xNHMy94g=
|
github.com/gobuffalo/genny v0.0.0-20180924032338-7af3a40f2252/go.mod h1:tUTQOogrr7tAQnhajMSH6rv1BVev34H2sa1xNHMy94g=
|
||||||
github.com/gobuffalo/genny v0.0.0-20181003150629-3786a0744c5d/go.mod h1:WAd8HmjMVrnkAZbmfgH5dLBUchsZfqzp/WS5sQz+uTM=
|
github.com/gobuffalo/genny v0.0.0-20181003150629-3786a0744c5d/go.mod h1:WAd8HmjMVrnkAZbmfgH5dLBUchsZfqzp/WS5sQz+uTM=
|
||||||
github.com/gobuffalo/genny v0.0.0-20181005145118-318a41a134cc/go.mod h1:WAd8HmjMVrnkAZbmfgH5dLBUchsZfqzp/WS5sQz+uTM=
|
github.com/gobuffalo/genny v0.0.0-20181005145118-318a41a134cc/go.mod h1:WAd8HmjMVrnkAZbmfgH5dLBUchsZfqzp/WS5sQz+uTM=
|
||||||
|
|
@ -100,6 +124,8 @@ github.com/gobuffalo/genny v0.0.0-20181203201232-849d2c9534ea/go.mod h1:wpNSANu9
|
||||||
github.com/gobuffalo/genny v0.0.0-20181206121324-d6fb8a0dbe36/go.mod h1:wpNSANu9UErftfiaAlz1pDZclrYzLtO5lALifODyjuM=
|
github.com/gobuffalo/genny v0.0.0-20181206121324-d6fb8a0dbe36/go.mod h1:wpNSANu9UErftfiaAlz1pDZclrYzLtO5lALifODyjuM=
|
||||||
github.com/gobuffalo/genny v0.0.0-20181207164119-84844398a37d/go.mod h1:y0ysCHGGQf2T3vOhCrGHheYN54Y/REj0ayd0Suf4C/8=
|
github.com/gobuffalo/genny v0.0.0-20181207164119-84844398a37d/go.mod h1:y0ysCHGGQf2T3vOhCrGHheYN54Y/REj0ayd0Suf4C/8=
|
||||||
github.com/gobuffalo/genny v0.0.0-20181211165820-e26c8466f14d/go.mod h1:sHnK+ZSU4e2feXP3PA29ouij6PUEiN+RCwECjCTB3yM=
|
github.com/gobuffalo/genny v0.0.0-20181211165820-e26c8466f14d/go.mod h1:sHnK+ZSU4e2feXP3PA29ouij6PUEiN+RCwECjCTB3yM=
|
||||||
|
github.com/gobuffalo/genny v0.0.0-20190104222617-a71664fc38e7/go.mod h1:QPsQ1FnhEsiU8f+O0qKWXz2RE4TiDqLVChWkBuh1WaY=
|
||||||
|
github.com/gobuffalo/genny v0.0.0-20190112155932-f31a84fcacf5/go.mod h1:CIaHCrSIuJ4il6ka3Hub4DR4adDrGoXGEEt2FbBxoIo=
|
||||||
github.com/gobuffalo/github_flavored_markdown v1.0.4/go.mod h1:uRowCdK+q8d/RF0Kt3/DSalaIXbb0De/dmTqMQdkQ4I=
|
github.com/gobuffalo/github_flavored_markdown v1.0.4/go.mod h1:uRowCdK+q8d/RF0Kt3/DSalaIXbb0De/dmTqMQdkQ4I=
|
||||||
github.com/gobuffalo/github_flavored_markdown v1.0.5/go.mod h1:U0643QShPF+OF2tJvYNiYDLDGDuQmJZXsf/bHOJPsMY=
|
github.com/gobuffalo/github_flavored_markdown v1.0.5/go.mod h1:U0643QShPF+OF2tJvYNiYDLDGDuQmJZXsf/bHOJPsMY=
|
||||||
github.com/gobuffalo/github_flavored_markdown v1.0.7/go.mod h1:w93Pd9Lz6LvyQXEG6DktTPHkOtCbr+arAD5mkwMzXLI=
|
github.com/gobuffalo/github_flavored_markdown v1.0.7/go.mod h1:w93Pd9Lz6LvyQXEG6DktTPHkOtCbr+arAD5mkwMzXLI=
|
||||||
|
|
@ -110,6 +136,7 @@ github.com/gobuffalo/licenser v0.0.0-20181027200154-58051a75da95/go.mod h1:Bzhaa
|
||||||
github.com/gobuffalo/licenser v0.0.0-20181109171355-91a2a7aac9a7/go.mod h1:m+Ygox92pi9bdg+gVaycvqE8RVSjZp7mWw75+K5NPHk=
|
github.com/gobuffalo/licenser v0.0.0-20181109171355-91a2a7aac9a7/go.mod h1:m+Ygox92pi9bdg+gVaycvqE8RVSjZp7mWw75+K5NPHk=
|
||||||
github.com/gobuffalo/licenser v0.0.0-20181128165715-cc7305f8abed/go.mod h1:oU9F9UCE+AzI/MueCKZamsezGOOHfSirltllOVeRTAE=
|
github.com/gobuffalo/licenser v0.0.0-20181128165715-cc7305f8abed/go.mod h1:oU9F9UCE+AzI/MueCKZamsezGOOHfSirltllOVeRTAE=
|
||||||
github.com/gobuffalo/licenser v0.0.0-20181203160806-fe900bbede07/go.mod h1:ph6VDNvOzt1CdfaWC+9XwcBnlSTBz2j49PBwum6RFaU=
|
github.com/gobuffalo/licenser v0.0.0-20181203160806-fe900bbede07/go.mod h1:ph6VDNvOzt1CdfaWC+9XwcBnlSTBz2j49PBwum6RFaU=
|
||||||
|
github.com/gobuffalo/licenser v0.0.0-20181211173111-f8a311c51159/go.mod h1:ve/Ue99DRuvnTaLq2zKa6F4KtHiYf7W046tDjuGYPfM=
|
||||||
github.com/gobuffalo/logger v0.0.0-20181022175615-46cfb361fc27/go.mod h1:8sQkgyhWipz1mIctHF4jTxmJh1Vxhp7mP8IqbljgJZo=
|
github.com/gobuffalo/logger v0.0.0-20181022175615-46cfb361fc27/go.mod h1:8sQkgyhWipz1mIctHF4jTxmJh1Vxhp7mP8IqbljgJZo=
|
||||||
github.com/gobuffalo/logger v0.0.0-20181027144941-73d08d2bb969/go.mod h1:7uGg2duHKpWnN4+YmyKBdLXfhopkAdVM6H3nKbyFbz8=
|
github.com/gobuffalo/logger v0.0.0-20181027144941-73d08d2bb969/go.mod h1:7uGg2duHKpWnN4+YmyKBdLXfhopkAdVM6H3nKbyFbz8=
|
||||||
github.com/gobuffalo/logger v0.0.0-20181027193913-9cf4dd0efe46/go.mod h1:7uGg2duHKpWnN4+YmyKBdLXfhopkAdVM6H3nKbyFbz8=
|
github.com/gobuffalo/logger v0.0.0-20181027193913-9cf4dd0efe46/go.mod h1:7uGg2duHKpWnN4+YmyKBdLXfhopkAdVM6H3nKbyFbz8=
|
||||||
|
|
@ -124,6 +151,7 @@ github.com/gobuffalo/meta v0.0.0-20181018192820-8c6cef77dab3/go.mod h1:E94EPzx9N
|
||||||
github.com/gobuffalo/meta v0.0.0-20181025145500-3a985a084b0a/go.mod h1:YDAKBud2FP7NZdruCSlmTmDOZbVSa6bpK7LJ/A/nlKg=
|
github.com/gobuffalo/meta v0.0.0-20181025145500-3a985a084b0a/go.mod h1:YDAKBud2FP7NZdruCSlmTmDOZbVSa6bpK7LJ/A/nlKg=
|
||||||
github.com/gobuffalo/meta v0.0.0-20181114191255-b130ebedd2f7/go.mod h1:K6cRZ29ozr4Btvsqkjvg5nDFTLOgTqf03KA70Ks0ypE=
|
github.com/gobuffalo/meta v0.0.0-20181114191255-b130ebedd2f7/go.mod h1:K6cRZ29ozr4Btvsqkjvg5nDFTLOgTqf03KA70Ks0ypE=
|
||||||
github.com/gobuffalo/meta v0.0.0-20181127070345-0d7e59dd540b/go.mod h1:RLO7tMvE0IAKAM8wny1aN12pvEKn7EtkBLkUZR00Qf8=
|
github.com/gobuffalo/meta v0.0.0-20181127070345-0d7e59dd540b/go.mod h1:RLO7tMvE0IAKAM8wny1aN12pvEKn7EtkBLkUZR00Qf8=
|
||||||
|
github.com/gobuffalo/meta v0.0.0-20190120163247-50bbb1fa260d/go.mod h1:KKsH44nIK2gA8p0PJmRT9GvWJUdphkDUA8AJEvFWiqM=
|
||||||
github.com/gobuffalo/mw-basicauth v1.0.3/go.mod h1:dg7+ilMZOKnQFHDefUzUHufNyTswVUviCBgF244C1+0=
|
github.com/gobuffalo/mw-basicauth v1.0.3/go.mod h1:dg7+ilMZOKnQFHDefUzUHufNyTswVUviCBgF244C1+0=
|
||||||
github.com/gobuffalo/mw-contenttype v0.0.0-20180802152300-74f5a47f4d56/go.mod h1:7EvcmzBbeCvFtQm5GqF9ys6QnCxz2UM1x0moiWLq1No=
|
github.com/gobuffalo/mw-contenttype v0.0.0-20180802152300-74f5a47f4d56/go.mod h1:7EvcmzBbeCvFtQm5GqF9ys6QnCxz2UM1x0moiWLq1No=
|
||||||
github.com/gobuffalo/mw-csrf v0.0.0-20180802151833-446ff26e108b/go.mod h1:sbGtb8DmDZuDUQoxjr8hG1ZbLtZboD9xsn6p77ppcHo=
|
github.com/gobuffalo/mw-csrf v0.0.0-20180802151833-446ff26e108b/go.mod h1:sbGtb8DmDZuDUQoxjr8hG1ZbLtZboD9xsn6p77ppcHo=
|
||||||
|
|
@ -148,14 +176,16 @@ github.com/gobuffalo/packr v1.15.1/go.mod h1:IeqicJ7jm8182yrVmNbM6PR4g79SjN9tZLH
|
||||||
github.com/gobuffalo/packr v1.19.0/go.mod h1:MstrNkfCQhd5o+Ct4IJ0skWlxN8emOq8DsoT1G98VIU=
|
github.com/gobuffalo/packr v1.19.0/go.mod h1:MstrNkfCQhd5o+Ct4IJ0skWlxN8emOq8DsoT1G98VIU=
|
||||||
github.com/gobuffalo/packr v1.20.0/go.mod h1:JDytk1t2gP+my1ig7iI4NcVaXr886+N0ecUga6884zw=
|
github.com/gobuffalo/packr v1.20.0/go.mod h1:JDytk1t2gP+my1ig7iI4NcVaXr886+N0ecUga6884zw=
|
||||||
github.com/gobuffalo/packr v1.21.0/go.mod h1:H00jGfj1qFKxscFJSw8wcL4hpQtPe1PfU2wa6sg/SR0=
|
github.com/gobuffalo/packr v1.21.0/go.mod h1:H00jGfj1qFKxscFJSw8wcL4hpQtPe1PfU2wa6sg/SR0=
|
||||||
github.com/gobuffalo/packr v1.21.9 h1:zBaEhCmJpYy/UdHGAGIC3vO5Uh7RW091le41+Ydcg4E=
|
github.com/gobuffalo/packr v1.22.0 h1:/YVd/GRGsu0QuoCJtlcWSVllobs4q3Xvx3nqxTvPyN0=
|
||||||
github.com/gobuffalo/packr v1.21.9/go.mod h1:GC76q6nMzRtR+AEN/VV4w0z2/4q7SOaEmXh3Ooa8sOE=
|
github.com/gobuffalo/packr v1.22.0/go.mod h1:Qr3Wtxr3+HuQEwWqlLnNW4t1oTvK+7Gc/Rnoi/lDFvA=
|
||||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.8/go.mod h1:y60QCdzwuMwO2R49fdQhsjCPv7tLQFR0ayzxxla9zes=
|
github.com/gobuffalo/packr/v2 v2.0.0-rc.8/go.mod h1:y60QCdzwuMwO2R49fdQhsjCPv7tLQFR0ayzxxla9zes=
|
||||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.9/go.mod h1:fQqADRfZpEsgkc7c/K7aMew3n4aF1Kji7+lIZeR98Fc=
|
github.com/gobuffalo/packr/v2 v2.0.0-rc.9/go.mod h1:fQqADRfZpEsgkc7c/K7aMew3n4aF1Kji7+lIZeR98Fc=
|
||||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.10/go.mod h1:4CWWn4I5T3v4c1OsJ55HbHlUEKNWMITG5iIkdr4Px4w=
|
github.com/gobuffalo/packr/v2 v2.0.0-rc.10/go.mod h1:4CWWn4I5T3v4c1OsJ55HbHlUEKNWMITG5iIkdr4Px4w=
|
||||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.11/go.mod h1:JoieH/3h3U4UmatmV93QmqyPUdf4wVM9HELaHEu+3fk=
|
github.com/gobuffalo/packr/v2 v2.0.0-rc.11/go.mod h1:JoieH/3h3U4UmatmV93QmqyPUdf4wVM9HELaHEu+3fk=
|
||||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.12/go.mod h1:FV1zZTsVFi1DSCboO36Xgs4pzCZBjB/tDV9Cz/lSaR8=
|
github.com/gobuffalo/packr/v2 v2.0.0-rc.12/go.mod h1:FV1zZTsVFi1DSCboO36Xgs4pzCZBjB/tDV9Cz/lSaR8=
|
||||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.13/go.mod h1:2Mp7GhBFMdJlOK8vGfl7SYtfMP3+5roE39ejlfjw0rA=
|
github.com/gobuffalo/packr/v2 v2.0.0-rc.13/go.mod h1:2Mp7GhBFMdJlOK8vGfl7SYtfMP3+5roE39ejlfjw0rA=
|
||||||
|
github.com/gobuffalo/packr/v2 v2.0.0-rc.14/go.mod h1:06otbrNvDKO1eNQ3b8hst+1010UooI2MFg+B2Ze4MV8=
|
||||||
|
github.com/gobuffalo/packr/v2 v2.0.0-rc.15/go.mod h1:IMe7H2nJvcKXSF90y4X1rjYIRlNMJYCxEhssBXNZwWs=
|
||||||
github.com/gobuffalo/plush v3.7.16+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
github.com/gobuffalo/plush v3.7.16+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
||||||
github.com/gobuffalo/plush v3.7.20+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
github.com/gobuffalo/plush v3.7.20+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
||||||
github.com/gobuffalo/plush v3.7.21+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
github.com/gobuffalo/plush v3.7.21+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
||||||
|
|
@ -167,6 +197,7 @@ github.com/gobuffalo/plush v3.7.32+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5s
|
||||||
github.com/gobuffalo/plushgen v0.0.0-20181128164830-d29dcb966cb2/go.mod h1:r9QwptTFnuvSaSRjpSp4S2/4e2D3tJhARYbvEBcKSb4=
|
github.com/gobuffalo/plushgen v0.0.0-20181128164830-d29dcb966cb2/go.mod h1:r9QwptTFnuvSaSRjpSp4S2/4e2D3tJhARYbvEBcKSb4=
|
||||||
github.com/gobuffalo/plushgen v0.0.0-20181203163832-9fc4964505c2/go.mod h1:opEdT33AA2HdrIwK1aibqnTJDVVKXC02Bar/GT1YRVs=
|
github.com/gobuffalo/plushgen v0.0.0-20181203163832-9fc4964505c2/go.mod h1:opEdT33AA2HdrIwK1aibqnTJDVVKXC02Bar/GT1YRVs=
|
||||||
github.com/gobuffalo/plushgen v0.0.0-20181207152837-eedb135bd51b/go.mod h1:Lcw7HQbEVm09sAQrCLzIxuhFbB3nAgp4c55E+UlynR0=
|
github.com/gobuffalo/plushgen v0.0.0-20181207152837-eedb135bd51b/go.mod h1:Lcw7HQbEVm09sAQrCLzIxuhFbB3nAgp4c55E+UlynR0=
|
||||||
|
github.com/gobuffalo/plushgen v0.0.0-20190104222512-177cd2b872b3/go.mod h1:tYxCozi8X62bpZyKXYHw1ncx2ZtT2nFvG42kuLwYjoc=
|
||||||
github.com/gobuffalo/pop v4.8.2+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
github.com/gobuffalo/pop v4.8.2+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||||
github.com/gobuffalo/pop v4.8.3+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
github.com/gobuffalo/pop v4.8.3+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||||
github.com/gobuffalo/pop v4.8.4+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
github.com/gobuffalo/pop v4.8.4+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||||
|
|
@ -180,25 +211,41 @@ github.com/gobuffalo/release v1.0.61/go.mod h1:mfIO38ujUNVDlBziIYqXquYfBF+8FDHUj
|
||||||
github.com/gobuffalo/release v1.0.72/go.mod h1:NP5NXgg/IX3M5XmHmWR99D687/3Dt9qZtTK/Lbwc1hU=
|
github.com/gobuffalo/release v1.0.72/go.mod h1:NP5NXgg/IX3M5XmHmWR99D687/3Dt9qZtTK/Lbwc1hU=
|
||||||
github.com/gobuffalo/release v1.1.1/go.mod h1:Sluak1Xd6kcp6snkluR1jeXAogdJZpFFRzTYRs/2uwg=
|
github.com/gobuffalo/release v1.1.1/go.mod h1:Sluak1Xd6kcp6snkluR1jeXAogdJZpFFRzTYRs/2uwg=
|
||||||
github.com/gobuffalo/release v1.1.3/go.mod h1:CuXc5/m+4zuq8idoDt1l4va0AXAn/OSs08uHOfMVr8E=
|
github.com/gobuffalo/release v1.1.3/go.mod h1:CuXc5/m+4zuq8idoDt1l4va0AXAn/OSs08uHOfMVr8E=
|
||||||
|
github.com/gobuffalo/release v1.1.6/go.mod h1:18naWa3kBsqO0cItXZNJuefCKOENpbbUIqRL1g+p6z0=
|
||||||
github.com/gobuffalo/shoulders v1.0.1/go.mod h1:V33CcVmaQ4gRUmHKwq1fiTXuf8Gp/qjQBUL5tHPmvbA=
|
github.com/gobuffalo/shoulders v1.0.1/go.mod h1:V33CcVmaQ4gRUmHKwq1fiTXuf8Gp/qjQBUL5tHPmvbA=
|
||||||
github.com/gobuffalo/syncx v0.0.0-20181120191700-98333ab04150/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
github.com/gobuffalo/syncx v0.0.0-20181120191700-98333ab04150/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
||||||
github.com/gobuffalo/syncx v0.0.0-20181120194010-558ac7de985f h1:S5EeH1reN93KR0L6TQvkRpu9YggCYXrUqFh1iEgvdC0=
|
github.com/gobuffalo/syncx v0.0.0-20181120194010-558ac7de985f h1:S5EeH1reN93KR0L6TQvkRpu9YggCYXrUqFh1iEgvdC0=
|
||||||
github.com/gobuffalo/syncx v0.0.0-20181120194010-558ac7de985f/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
github.com/gobuffalo/syncx v0.0.0-20181120194010-558ac7de985f/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
||||||
github.com/gobuffalo/tags v2.0.11+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
|
github.com/gobuffalo/tags v2.0.11+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
|
||||||
github.com/gobuffalo/tags v2.0.14+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
|
github.com/gobuffalo/tags v2.0.14+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
|
||||||
|
github.com/gobuffalo/tags v2.0.15+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
|
||||||
github.com/gobuffalo/uuid v2.0.3+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE=
|
github.com/gobuffalo/uuid v2.0.3+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE=
|
||||||
github.com/gobuffalo/uuid v2.0.4+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE=
|
github.com/gobuffalo/uuid v2.0.4+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE=
|
||||||
github.com/gobuffalo/uuid v2.0.5+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE=
|
github.com/gobuffalo/uuid v2.0.5+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE=
|
||||||
github.com/gobuffalo/validate v2.0.3+incompatible/go.mod h1:N+EtDe0J8252BgfzQUChBgfd6L93m9weay53EWFVsMM=
|
github.com/gobuffalo/validate v2.0.3+incompatible/go.mod h1:N+EtDe0J8252BgfzQUChBgfd6L93m9weay53EWFVsMM=
|
||||||
github.com/gobuffalo/x v0.0.0-20181003152136-452098b06085/go.mod h1:WevpGD+5YOreDJznWevcn8NTmQEW5STSBgIkpkjzqXc=
|
github.com/gobuffalo/x v0.0.0-20181003152136-452098b06085/go.mod h1:WevpGD+5YOreDJznWevcn8NTmQEW5STSBgIkpkjzqXc=
|
||||||
github.com/gobuffalo/x v0.0.0-20181007152206-913e47c59ca7/go.mod h1:9rDPXaB3kXdKWzMc4odGQQdG2e2DIEmANy5aSJ9yesY=
|
github.com/gobuffalo/x v0.0.0-20181007152206-913e47c59ca7/go.mod h1:9rDPXaB3kXdKWzMc4odGQQdG2e2DIEmANy5aSJ9yesY=
|
||||||
github.com/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedyHx0uzKwA=
|
|
||||||
github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||||
|
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||||
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
|
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
|
||||||
|
|
@ -211,13 +258,18 @@ github.com/gotify/configor v1.0.2-0.20190112111140-7d9c7c7e6233 h1:ZFYA2/LqyzFP2
|
||||||
github.com/gotify/configor v1.0.2-0.20190112111140-7d9c7c7e6233/go.mod h1:Sclq5yNfX/DJHu0TR2k0+Hi34YxsuKTacgrY3z83HoU=
|
github.com/gotify/configor v1.0.2-0.20190112111140-7d9c7c7e6233/go.mod h1:Sclq5yNfX/DJHu0TR2k0+Hi34YxsuKTacgrY3z83HoU=
|
||||||
github.com/gotify/location v0.0.0-20170722210143-03bc4ad20437 h1:4qMhogAexRcnvdoY9O1RoCuuuNEhDF25jtbGIWPtcms=
|
github.com/gotify/location v0.0.0-20170722210143-03bc4ad20437 h1:4qMhogAexRcnvdoY9O1RoCuuuNEhDF25jtbGIWPtcms=
|
||||||
github.com/gotify/location v0.0.0-20170722210143-03bc4ad20437/go.mod h1:5JgfyQg+71Ck3uXX/4FBHc4YxdKZ9shU8gs2AUj7Nj0=
|
github.com/gotify/location v0.0.0-20170722210143-03bc4ad20437/go.mod h1:5JgfyQg+71Ck3uXX/4FBHc4YxdKZ9shU8gs2AUj7Nj0=
|
||||||
github.com/h2non/filetype v1.0.5 h1:Esu2EFM5vrzNynnGQpj0nxhCkzVQh2HRY7AXUh/dyJM=
|
github.com/gotify/plugin-api v1.0.0 h1:kab40p2TEPLzjmcafOc7JOz75aTsYQyS2PXtElH8xmI=
|
||||||
github.com/h2non/filetype v1.0.5/go.mod h1:isekKqOuhMj+s/7r3rIeTErIRy4Rub5uBWHfvMusLMU=
|
github.com/gotify/plugin-api v1.0.0/go.mod h1:xZfEyqVK/Zvu3RwA/CtpuiwFmzFDxifrrqMaH9BHnyU=
|
||||||
|
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||||
|
github.com/h2non/filetype v1.0.6 h1:g84/+gdkAT1hnYO+tHpCLoikm13Ju55OkN4KCb1uGEQ=
|
||||||
|
github.com/h2non/filetype v1.0.6/go.mod h1:isekKqOuhMj+s/7r3rIeTErIRy4Rub5uBWHfvMusLMU=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
||||||
github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||||
|
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||||
github.com/jinzhu/gorm v1.9.2 h1:lCvgEaqe/HVE+tjAR2mt4HbbHAZsQOv3XAZiEZV37iw=
|
github.com/jinzhu/gorm v1.9.2 h1:lCvgEaqe/HVE+tjAR2mt4HbbHAZsQOv3XAZiEZV37iw=
|
||||||
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
||||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
|
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
|
||||||
|
|
@ -232,7 +284,9 @@ github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswD
|
||||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
|
github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
|
||||||
github.com/karrick/godirwalk v1.7.7/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
|
github.com/karrick/godirwalk v1.7.7/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
|
||||||
|
github.com/karrick/godirwalk v1.7.8/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
|
@ -256,7 +310,6 @@ github.com/markbates/inflect v1.0.4/go.mod h1:1fR9+pO2KHEO9ZRtto13gDwwZaAKstQzfe
|
||||||
github.com/markbates/oncer v0.0.0-20180924031910-e862a676800b/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
github.com/markbates/oncer v0.0.0-20180924031910-e862a676800b/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||||
github.com/markbates/oncer v0.0.0-20180924034138-723ad0170a46/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
github.com/markbates/oncer v0.0.0-20180924034138-723ad0170a46/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||||
github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2 h1:JgVTCPf0uBVcUSWpyXmGpgOc62nK5HWUBKAGc3Qqa5k=
|
|
||||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||||
github.com/markbates/refresh v1.4.10/go.mod h1:NDPHvotuZmTmesXxr95C9bjlw1/0frJwtME2dzcVKhc=
|
github.com/markbates/refresh v1.4.10/go.mod h1:NDPHvotuZmTmesXxr95C9bjlw1/0frJwtME2dzcVKhc=
|
||||||
github.com/markbates/safe v1.0.0/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
github.com/markbates/safe v1.0.0/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
||||||
|
|
@ -269,7 +322,9 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
|
||||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
||||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
|
@ -278,37 +333,69 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/monoculum/formam v0.0.0-20180901015400-4e68be1d79ba/go.mod h1:RKgILGEJq24YyJ2ban8EO0RUVSJlF1pGsEvoLEACr/Q=
|
github.com/monoculum/formam v0.0.0-20180901015400-4e68be1d79ba/go.mod h1:RKgILGEJq24YyJ2ban8EO0RUVSJlF1pGsEvoLEACr/Q=
|
||||||
|
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||||
|
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||||
github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
|
github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.0.0 h1:o4VLZ5jqHE+HahLT6drNtSGTrrUA3wPBmtpgqtdbClo=
|
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 h1:x7xEyJDP7Hv3LVgvWhzioQqbC/KtuUhTigKlH/8ehhE=
|
||||||
|
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||||
github.com/rogpeppe/go-internal v1.0.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.0.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rogpeppe/go-internal v1.1.0 h1:g0fH8RicVgNl+zVZDCDfbdWxAWoAEJyI7I3TZYXFiig=
|
||||||
|
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
|
github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||||
|
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||||
|
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||||
|
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||||
|
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||||
|
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||||
github.com/shurcooL/highlight_go v0.0.0-20170515013102-78fb10f4a5f8/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
github.com/shurcooL/highlight_go v0.0.0-20170515013102-78fb10f4a5f8/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||||
|
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||||
|
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||||
|
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||||
|
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||||
|
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||||
|
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||||
|
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||||
|
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||||
|
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||||
github.com/shurcooL/octicon v0.0.0-20180602230221-c42b0e3b24d9/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
github.com/shurcooL/octicon v0.0.0-20180602230221-c42b0e3b24d9/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||||
|
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||||
|
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||||
|
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
|
github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
|
||||||
github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
|
github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/afero v1.2.0/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||||
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
|
|
@ -322,11 +409,16 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648=
|
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
|
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4=
|
||||||
|
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
github.com/unrolled/secure v0.0.0-20180918153822-f340ee86eb8b/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA=
|
github.com/unrolled/secure v0.0.0-20180918153822-f340ee86eb8b/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA=
|
||||||
github.com/unrolled/secure v0.0.0-20181005190816-ff9db2ff917f/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA=
|
github.com/unrolled/secure v0.0.0-20181005190816-ff9db2ff917f/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||||
|
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||||
|
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
|
@ -335,14 +427,20 @@ golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnf
|
||||||
golang.org/x/crypto v0.0.0-20181024171144-74cb1d3d52f4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181024171144-74cb1d3d52f4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181025113841-85e1b3f9139a/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181025113841-85e1b3f9139a/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M=
|
golang.org/x/crypto v0.0.0-20190102171810-8d7daa0c54b3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 h1:MQ/ZZiDsUapFFiMS+vzwXkCTeEKaum+Do5rINYJDmxc=
|
||||||
|
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180921000356-2f5d2388922f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180921000356-2f5d2388922f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
|
@ -350,15 +448,24 @@ golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73r
|
||||||
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181207154023-610586996380 h1:zPQexyRtNYBc7bcHmehl1dH6TB3qn8zytv8cBGLDNY0=
|
|
||||||
golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM=
|
||||||
|
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
|
@ -371,12 +478,20 @@ golang.org/x/sys v0.0.0-20181024145615-5cd93ef61a7c/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||||
golang.org/x/sys v0.0.0-20181025063200-d989b31c8746/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181025063200-d989b31c8746/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181026064943-731415f00dce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181026064943-731415f00dce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181106135930-3a76605856fd/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181106135930-3a76605856fd/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e h1:njOxP/wVblhCLIUhjHXf6X+dzTt5OQ3vMQo9mkOIKIo=
|
|
||||||
golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0=
|
||||||
|
golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181003024731-2f84ea8ef872/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20181003024731-2f84ea8ef872/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181006002542-f60d9635b16a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20181006002542-f60d9635b16a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|
@ -385,6 +500,7 @@ golang.org/x/tools v0.0.0-20181013182035-5e66757b835f/go.mod h1:n7NCudcB/nEzxVGm
|
||||||
golang.org/x/tools v0.0.0-20181017214349-06f26fdaaa28/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20181017214349-06f26fdaaa28/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181024171208-a2dc47679d30/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20181024171208-a2dc47679d30/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181026183834-f60e5f99f081/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20181026183834-f60e5f99f081/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181105230042-78dc5bac0cac/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20181105230042-78dc5bac0cac/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181107215632-34b416bd17b3/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20181107215632-34b416bd17b3/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181114190951-94339b83286c/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20181114190951-94339b83286c/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|
@ -396,9 +512,26 @@ golang.org/x/tools v0.0.0-20181205224935-3576414c54a4/go.mod h1:n7NCudcB/nEzxVGm
|
||||||
golang.org/x/tools v0.0.0-20181206194817-bcd4e47d0288/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20181206194817-bcd4e47d0288/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181207183836-8bc39b988060/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20181207183836-8bc39b988060/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181212172921-837e80568c09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20181212172921-837e80568c09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190102213336-ca9055ed7d04/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190104182027-498d95493402/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190111214448-fc1d57b08d7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190118193359-16909d206f00/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||||
|
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||||
|
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||||
|
google.golang.org/genproto v0.0.0-20190122154452-ba6ebe99b011/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||||
|
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||||
|
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||||
|
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|
@ -412,10 +545,14 @@ gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8
|
||||||
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
|
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
|
||||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||||
gopkg.in/h2non/filetype.v1 v1.0.5 h1:CC1jjJjoEhNVbMhXYalmGBhOBK2V70Q1N850wt/98/Y=
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
gopkg.in/h2non/filetype.v1 v1.0.5/go.mod h1:M0yem4rwSX5lLVrkEuRRp2/NinFMD5vgJ4DlAhZcfNo=
|
|
||||||
gopkg.in/mail.v2 v2.0.0-20180731213649-a0242b2233b4/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
gopkg.in/mail.v2 v2.0.0-20180731213649-a0242b2233b4/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||||
|
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||||
|
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@ type Application struct {
|
||||||
// required: true
|
// required: true
|
||||||
// example: Backup server for the interwebs
|
// example: Backup server for the interwebs
|
||||||
Description string `form:"description" query:"description" json:"description"`
|
Description string `form:"description" query:"description" json:"description"`
|
||||||
|
// Whether the application is an internal application. Internal applications should not be deleted.
|
||||||
|
//
|
||||||
|
// read only: true
|
||||||
|
// required: true
|
||||||
|
// example: false
|
||||||
|
Internal bool `form:"internal" query:"internal" json:"internal"`
|
||||||
// The image of the application.
|
// The image of the application.
|
||||||
//
|
//
|
||||||
// read only: true
|
// read only: true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
// PluginConf holds information about the plugin
|
||||||
|
type PluginConf struct {
|
||||||
|
ID uint `gorm:"primary_key;AUTO_INCREMENT;index"`
|
||||||
|
UserID uint
|
||||||
|
ModulePath string
|
||||||
|
Token string `gorm:"unique_index"`
|
||||||
|
ApplicationID uint
|
||||||
|
Enabled bool
|
||||||
|
Config []byte
|
||||||
|
Storage []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginConfExternal Model
|
||||||
|
//
|
||||||
|
// Holds information about a plugin instance for one user.
|
||||||
|
//
|
||||||
|
// swagger:model PluginConf
|
||||||
|
type PluginConfExternal struct {
|
||||||
|
// The plugin id.
|
||||||
|
//
|
||||||
|
// read only: true
|
||||||
|
// required: true
|
||||||
|
// example: 25
|
||||||
|
ID uint `json:"id"`
|
||||||
|
// The plugin name.
|
||||||
|
//
|
||||||
|
// read only: true
|
||||||
|
// required: true
|
||||||
|
// example: RSS poller
|
||||||
|
Name string `json:"name"`
|
||||||
|
// The user name. For login.
|
||||||
|
//
|
||||||
|
// required: true
|
||||||
|
// example: P1234
|
||||||
|
Token string `binding:"required" json:"token" query:"token" form:"token"`
|
||||||
|
// The module path of the plugin.
|
||||||
|
//
|
||||||
|
// example: github.com/gotify/server/plugin/example/echo
|
||||||
|
// read only: true
|
||||||
|
// required: true
|
||||||
|
ModulePath string `json:"modulePath" form:"modulePath" query:"modulePath"`
|
||||||
|
// The author of the plugin.
|
||||||
|
//
|
||||||
|
// example: jmattheis
|
||||||
|
// read only: true
|
||||||
|
Author string `json:"author,omitempty" form:"author" query:"author"`
|
||||||
|
// The website of the plugin.
|
||||||
|
//
|
||||||
|
// example: gotify.net
|
||||||
|
// read only: true
|
||||||
|
Website string `json:"website,omitempty" form:"website" query:"website"`
|
||||||
|
// The license of the plugin.
|
||||||
|
//
|
||||||
|
// example: MIT
|
||||||
|
// read only: true
|
||||||
|
License string `json:"license,omitempty" form:"license" query:"license"`
|
||||||
|
// Whether the plugin instance is enabled.
|
||||||
|
//
|
||||||
|
// example: true
|
||||||
|
// required: true
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
// Capabilities the plugin provides
|
||||||
|
//
|
||||||
|
// example: ["webhook","display"]
|
||||||
|
// required: true
|
||||||
|
Capabilities []string `json:"capabilities"`
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ type User struct {
|
||||||
Admin bool
|
Admin bool
|
||||||
Applications []Application
|
Applications []Application
|
||||||
Clients []Client
|
Clients []Client
|
||||||
|
Plugins []PluginConf
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserExternal Model
|
// UserExternal Model
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package compat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Capability is a capability the plugin provides
|
||||||
|
type Capability string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Messenger sends notifications
|
||||||
|
Messenger = Capability("messenger")
|
||||||
|
// Configurer are consigurables
|
||||||
|
Configurer = Capability("configurer")
|
||||||
|
// Storager stores data
|
||||||
|
Storager = Capability("storager")
|
||||||
|
// Webhooker registers webhooks
|
||||||
|
Webhooker = Capability("webhooker")
|
||||||
|
// Displayer displays instructions
|
||||||
|
Displayer = Capability("displayer")
|
||||||
|
)
|
||||||
|
|
||||||
|
// PluginInstance is an encapsulation layer of plugin instances of different backends
|
||||||
|
type PluginInstance interface {
|
||||||
|
Enable() error
|
||||||
|
Disable() error
|
||||||
|
|
||||||
|
// GetDisplay see Displayer
|
||||||
|
GetDisplay(location *url.URL) string
|
||||||
|
|
||||||
|
// DefaultConfig see Configurer
|
||||||
|
DefaultConfig() interface{}
|
||||||
|
// ValidateAndSetConfig see Configurer
|
||||||
|
ValidateAndSetConfig(c interface{}) error
|
||||||
|
|
||||||
|
// SetMessageHandler see Messenger#SetMessageHandler
|
||||||
|
SetMessageHandler(h MessageHandler)
|
||||||
|
|
||||||
|
// RegisterWebhook see Webhooker#RegisterWebhook
|
||||||
|
RegisterWebhook(basePath string, mux *gin.RouterGroup)
|
||||||
|
|
||||||
|
// SetStorageHandler see Storager#SetStorageHandler.
|
||||||
|
SetStorageHandler(handler StorageHandler)
|
||||||
|
|
||||||
|
// Returns the supported modules, f.ex. storager
|
||||||
|
Supports() Capabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasSupport tests a PluginInstance for a capability
|
||||||
|
func HasSupport(p PluginInstance, toCheck Capability) bool {
|
||||||
|
for _, module := range p.Supports() {
|
||||||
|
if module == toCheck {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capabilities is a slice of module
|
||||||
|
type Capabilities []Capability
|
||||||
|
|
||||||
|
// Strings converts []Module to []string
|
||||||
|
func (m Capabilities) Strings() []string {
|
||||||
|
var result []string
|
||||||
|
for _, module := range m {
|
||||||
|
result = append(result, string(module))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageHandler see plugin.MessageHandler.
|
||||||
|
type MessageHandler interface {
|
||||||
|
// SendMessage see plugin.MessageHandler
|
||||||
|
SendMessage(msg Message) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageHandler see plugin.StorageHandler.
|
||||||
|
type StorageHandler interface {
|
||||||
|
Save(b []byte) error
|
||||||
|
Load() ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message describes a message to be send by MessageHandler#SendMessage.
|
||||||
|
type Message struct {
|
||||||
|
Message string
|
||||||
|
Title string
|
||||||
|
Priority int
|
||||||
|
Extras map[string]interface{}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package compat
|
||||||
|
|
||||||
|
// Plugin is an abstraction of plugin handler
|
||||||
|
type Plugin interface {
|
||||||
|
PluginInfo() Info
|
||||||
|
NewPluginInstance(ctx UserContext) PluginInstance
|
||||||
|
APIVersion() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info is the plugin info
|
||||||
|
type Info struct {
|
||||||
|
Version string
|
||||||
|
Author string
|
||||||
|
Name string
|
||||||
|
Website string
|
||||||
|
Description string
|
||||||
|
License string
|
||||||
|
ModulePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Info) String() string {
|
||||||
|
if c.Name != "" {
|
||||||
|
return c.Name
|
||||||
|
}
|
||||||
|
return c.ModulePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserContext is the user context used to create plugin instance.
|
||||||
|
type UserContext struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
Admin bool
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package compat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
const examplePluginPath = "github.com/gotify/server/plugin/example/echo"
|
||||||
|
|
||||||
|
func TestPluginInfoStringer(t *testing.T) {
|
||||||
|
info := Info{
|
||||||
|
ModulePath: examplePluginPath,
|
||||||
|
}
|
||||||
|
assert.Equal(t, examplePluginPath, info.String())
|
||||||
|
info.Name = "test name"
|
||||||
|
assert.Equal(t, "test name", info.String())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
package compat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
papiv1 "github.com/gotify/plugin-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PluginV1 is an abstraction of a plugin written in the v1 plugin API. Exported for testing purposes only.
|
||||||
|
type PluginV1 struct {
|
||||||
|
Info papiv1.Info
|
||||||
|
Constructor func(ctx papiv1.UserContext) papiv1.Plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIVersion returns the API version
|
||||||
|
func (c PluginV1) APIVersion() string {
|
||||||
|
return "v1"
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginInfo implements conpat/Plugin
|
||||||
|
func (c PluginV1) PluginInfo() Info {
|
||||||
|
return Info{
|
||||||
|
Version: c.Info.Version,
|
||||||
|
Author: c.Info.Author,
|
||||||
|
Name: c.Info.Name,
|
||||||
|
Website: c.Info.Website,
|
||||||
|
Description: c.Info.Description,
|
||||||
|
License: c.Info.License,
|
||||||
|
ModulePath: c.Info.ModulePath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPluginInstance implements compat/Plugin
|
||||||
|
func (c PluginV1) NewPluginInstance(ctx UserContext) PluginInstance {
|
||||||
|
instance := c.Constructor(papiv1.UserContext{
|
||||||
|
ID: ctx.ID,
|
||||||
|
Name: ctx.Name,
|
||||||
|
Admin: ctx.Admin,
|
||||||
|
})
|
||||||
|
|
||||||
|
compat := &PluginV1Instance{
|
||||||
|
instance: instance,
|
||||||
|
}
|
||||||
|
|
||||||
|
if displayer, ok := instance.(papiv1.Displayer); ok {
|
||||||
|
compat.displayer = displayer
|
||||||
|
}
|
||||||
|
|
||||||
|
if messenger, ok := instance.(papiv1.Messenger); ok {
|
||||||
|
compat.messenger = messenger
|
||||||
|
}
|
||||||
|
|
||||||
|
if configurer, ok := instance.(papiv1.Configurer); ok {
|
||||||
|
compat.configurer = configurer
|
||||||
|
}
|
||||||
|
|
||||||
|
if storager, ok := instance.(papiv1.Storager); ok {
|
||||||
|
compat.storager = storager
|
||||||
|
}
|
||||||
|
|
||||||
|
if webhooker, ok := instance.(papiv1.Webhooker); ok {
|
||||||
|
compat.webhooker = webhooker
|
||||||
|
}
|
||||||
|
|
||||||
|
return compat
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginV1Instance is an adapter for plugin using v1 API
|
||||||
|
type PluginV1Instance struct {
|
||||||
|
instance papiv1.Plugin
|
||||||
|
messenger papiv1.Messenger
|
||||||
|
configurer papiv1.Configurer
|
||||||
|
storager papiv1.Storager
|
||||||
|
webhooker papiv1.Webhooker
|
||||||
|
displayer papiv1.Displayer
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig see papiv1.Configurer
|
||||||
|
func (c *PluginV1Instance) DefaultConfig() interface{} {
|
||||||
|
if c.configurer != nil {
|
||||||
|
return c.configurer.DefaultConfig()
|
||||||
|
}
|
||||||
|
return struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAndSetConfig see papiv1.Configurer
|
||||||
|
func (c *PluginV1Instance) ValidateAndSetConfig(config interface{}) error {
|
||||||
|
if c.configurer != nil {
|
||||||
|
return c.configurer.ValidateAndSetConfig(config)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDisplay see papiv1.Displayer
|
||||||
|
func (c *PluginV1Instance) GetDisplay(location *url.URL) string {
|
||||||
|
if c.displayer != nil {
|
||||||
|
return c.displayer.GetDisplay(location)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMessageHandler see papiv1.Messenger
|
||||||
|
func (c *PluginV1Instance) SetMessageHandler(h MessageHandler) {
|
||||||
|
if c.messenger != nil {
|
||||||
|
c.messenger.SetMessageHandler(&PluginV1MessageHandler{WrapperHandler: h})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterWebhook see papiv1.Webhooker
|
||||||
|
func (c *PluginV1Instance) RegisterWebhook(basePath string, mux *gin.RouterGroup) {
|
||||||
|
if c.webhooker != nil {
|
||||||
|
c.webhooker.RegisterWebhook(basePath, mux)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStorageHandler see papiv1.Storager
|
||||||
|
func (c *PluginV1Instance) SetStorageHandler(handler StorageHandler) {
|
||||||
|
if c.storager != nil {
|
||||||
|
c.storager.SetStorageHandler(&PluginV1StorageHandler{WrapperHandler: handler})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supports returns a slice of capabilities the plugin instance provides
|
||||||
|
func (c *PluginV1Instance) Supports() Capabilities {
|
||||||
|
modules := Capabilities{}
|
||||||
|
if c.configurer != nil {
|
||||||
|
modules = append(modules, Configurer)
|
||||||
|
}
|
||||||
|
if c.displayer != nil {
|
||||||
|
modules = append(modules, Displayer)
|
||||||
|
}
|
||||||
|
if c.messenger != nil {
|
||||||
|
modules = append(modules, Messenger)
|
||||||
|
}
|
||||||
|
if c.storager != nil {
|
||||||
|
modules = append(modules, Storager)
|
||||||
|
}
|
||||||
|
if c.webhooker != nil {
|
||||||
|
modules = append(modules, Webhooker)
|
||||||
|
}
|
||||||
|
return modules
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginV1MessageHandler is an adapter for messenger plugin handler using v1 API
|
||||||
|
type PluginV1MessageHandler struct {
|
||||||
|
WrapperHandler MessageHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessage implements papiv1.MessageHandler
|
||||||
|
func (c *PluginV1MessageHandler) SendMessage(msg papiv1.Message) error {
|
||||||
|
return c.WrapperHandler.SendMessage(Message{
|
||||||
|
Message: msg.Message,
|
||||||
|
Priority: msg.Priority,
|
||||||
|
Title: msg.Title,
|
||||||
|
Extras: msg.Extras,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable implements wrapper.Plugin
|
||||||
|
func (c *PluginV1Instance) Enable() error {
|
||||||
|
return c.instance.Enable()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable implements wrapper.Plugin
|
||||||
|
func (c *PluginV1Instance) Disable() error {
|
||||||
|
return c.instance.Disable()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginV1StorageHandler is a wrapper for v1 storage handler
|
||||||
|
type PluginV1StorageHandler struct {
|
||||||
|
WrapperHandler StorageHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save implements wrapper.Storager
|
||||||
|
func (c *PluginV1StorageHandler) Save(b []byte) error {
|
||||||
|
return c.WrapperHandler.Save(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load implements wrapper.Storager
|
||||||
|
func (c *PluginV1StorageHandler) Load() ([]byte, error) {
|
||||||
|
return c.WrapperHandler.Load()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
package compat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
papiv1 "github.com/gotify/plugin-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type v1MockInstance struct {
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *v1MockInstance) Enable() error {
|
||||||
|
c.Enabled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *v1MockInstance) Disable() error {
|
||||||
|
c.Enabled = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type V1WrapperSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
i PluginV1Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *V1WrapperSuite) SetupSuite() {
|
||||||
|
inst := new(v1MockInstance)
|
||||||
|
s.i.instance = inst
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *V1WrapperSuite) TestConfigurer_notSupported_expectEmpty() {
|
||||||
|
assert.Equal(s.T(), struct{}{}, s.i.DefaultConfig())
|
||||||
|
assert.Nil(s.T(), s.i.ValidateAndSetConfig(struct{}{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *V1WrapperSuite) TestDisplayer_notSupported_expectEmpty() {
|
||||||
|
assert.Equal(s.T(), "", s.i.GetDisplay(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
type v1StorageHandler struct {
|
||||||
|
storage []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *v1StorageHandler) Save(b []byte) error {
|
||||||
|
c.storage = b
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *v1StorageHandler) Load() ([]byte, error) {
|
||||||
|
return c.storage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type v1Storager struct {
|
||||||
|
handler papiv1.StorageHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *v1Storager) Enable() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *v1Storager) Disable() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *v1Storager) SetStorageHandler(h papiv1.StorageHandler) {
|
||||||
|
c.handler = h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *V1WrapperSuite) TestStorager() {
|
||||||
|
storager := new(v1Storager)
|
||||||
|
s.i.storager = storager
|
||||||
|
|
||||||
|
s.i.SetStorageHandler(new(v1StorageHandler))
|
||||||
|
|
||||||
|
assert.Nil(s.T(), storager.handler.Save([]byte("test")))
|
||||||
|
storage, err := storager.handler.Load()
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), "test", string(storage))
|
||||||
|
}
|
||||||
|
|
||||||
|
type v1MessengerHandler struct {
|
||||||
|
msgSent Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *v1MessengerHandler) SendMessage(msg Message) error {
|
||||||
|
c.msgSent = msg
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type v1Messenger struct {
|
||||||
|
handler papiv1.MessageHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *v1Messenger) Enable() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *v1Messenger) Disable() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *v1Messenger) SetMessageHandler(h papiv1.MessageHandler) {
|
||||||
|
c.handler = h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *V1WrapperSuite) TestMessenger_sendMessageWithExtras() {
|
||||||
|
messenger := new(v1Messenger)
|
||||||
|
s.i.messenger = messenger
|
||||||
|
|
||||||
|
handler := new(v1MessengerHandler)
|
||||||
|
s.i.SetMessageHandler(handler)
|
||||||
|
|
||||||
|
msg := papiv1.Message{
|
||||||
|
Title: "test message",
|
||||||
|
Message: "test",
|
||||||
|
Priority: 2,
|
||||||
|
Extras: map[string]interface{}{
|
||||||
|
"test::string": "test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Nil(s.T(), messenger.handler.SendMessage(msg))
|
||||||
|
assert.Equal(s.T(), Message{
|
||||||
|
Title: "test message",
|
||||||
|
Message: "test",
|
||||||
|
Priority: 2,
|
||||||
|
Extras: map[string]interface{}{
|
||||||
|
"test::string": "test",
|
||||||
|
},
|
||||||
|
}, handler.msgSent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *V1WrapperSuite) TestMessenger_sendMessageWithoutExtras() {
|
||||||
|
messenger := new(v1Messenger)
|
||||||
|
s.i.messenger = messenger
|
||||||
|
|
||||||
|
handler := new(v1MessengerHandler)
|
||||||
|
s.i.SetMessageHandler(handler)
|
||||||
|
|
||||||
|
msg := papiv1.Message{
|
||||||
|
Title: "test message",
|
||||||
|
Message: "test",
|
||||||
|
Priority: 2,
|
||||||
|
Extras: nil,
|
||||||
|
}
|
||||||
|
assert.Nil(s.T(), messenger.handler.SendMessage(msg))
|
||||||
|
assert.Equal(s.T(), Message{
|
||||||
|
Title: "test message",
|
||||||
|
Message: "test",
|
||||||
|
Priority: 2,
|
||||||
|
Extras: nil,
|
||||||
|
}, handler.msgSent)
|
||||||
|
}
|
||||||
|
func TestV1Wrapper(t *testing.T) {
|
||||||
|
suite.Run(t, new(V1WrapperSuite))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package compat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"plugin"
|
||||||
|
|
||||||
|
papiv1 "github.com/gotify/plugin-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wrap wraps around a raw go plugin to provide typesafe access.
|
||||||
|
func Wrap(p *plugin.Plugin) (Plugin, error) {
|
||||||
|
getInfoHandle, err := p.Lookup("GetGotifyPluginInfo")
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("missing GetGotifyPluginInfo symbol")
|
||||||
|
}
|
||||||
|
switch getInfoHandle := getInfoHandle.(type) {
|
||||||
|
case func() papiv1.Info:
|
||||||
|
v1 := PluginV1{}
|
||||||
|
|
||||||
|
v1.Info = getInfoHandle()
|
||||||
|
newInstanceHandle, err := p.Lookup("NewGotifyPluginInstance")
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("missing NewGotifyPluginInstance symbol")
|
||||||
|
}
|
||||||
|
constructor, ok := newInstanceHandle.(func(ctx papiv1.UserContext) papiv1.Plugin)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("NewGotifyPluginInstance signature mismatch, func(ctx plugin.UserContext) plugin.Plugin expected, got %T", newInstanceHandle)
|
||||||
|
}
|
||||||
|
v1.Constructor = constructor
|
||||||
|
return v1, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown plugin version (unrecogninzed GetGotifyPluginInfo signature %T)", getInfoHandle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
// +build linux darwin
|
||||||
|
|
||||||
|
package compat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"plugin"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gotify/server/test"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CompatSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
p Plugin
|
||||||
|
tmpDir test.TmpDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CompatSuite) SetupSuite() {
|
||||||
|
s.tmpDir = test.NewTmpDir("gotify_compatsuite")
|
||||||
|
|
||||||
|
test.WithWd(path.Join(test.GetProjectDir(), "./plugin/example/echo"), func(origWd string) {
|
||||||
|
exec.Command("go", "get", "-d").Run()
|
||||||
|
goBuildFlags := []string{"build", "-buildmode=plugin", "-o=" + s.tmpDir.Path("echo.so")}
|
||||||
|
|
||||||
|
for _, extraFlag := range extraGoBuildFlags {
|
||||||
|
goBuildFlags = append(goBuildFlags, extraFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("go", goBuildFlags...)
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
assert.Nil(s.T(), cmd.Run())
|
||||||
|
})
|
||||||
|
|
||||||
|
plugin, err := plugin.Open(s.tmpDir.Path("echo.so"))
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
wrappedPlugin, err := Wrap(plugin)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
s.p = wrappedPlugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CompatSuite) TearDownSuite() {
|
||||||
|
assert.Nil(s.T(), s.tmpDir.Clean())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CompatSuite) TestGetPluginAPIVersion() {
|
||||||
|
assert.Equal(s.T(), "v1", s.p.APIVersion())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CompatSuite) TestGetPluginInfo() {
|
||||||
|
info := s.p.PluginInfo()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), examplePluginPath, info.ModulePath)
|
||||||
|
assert.True(s.T(), info.String() != "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CompatSuite) TestInstantiatePlugin() {
|
||||||
|
inst := s.p.NewPluginInstance(UserContext{
|
||||||
|
ID: 1,
|
||||||
|
Name: "test",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NotNil(s.T(), inst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CompatSuite) TestGetCapabilities() {
|
||||||
|
inst := s.p.NewPluginInstance(UserContext{
|
||||||
|
ID: 2,
|
||||||
|
Name: "test2",
|
||||||
|
})
|
||||||
|
|
||||||
|
c := inst.Supports()
|
||||||
|
|
||||||
|
assert.Contains(s.T(), c, Webhooker)
|
||||||
|
assert.Contains(s.T(), c.Strings(), string(Webhooker))
|
||||||
|
assert.True(s.T(), HasSupport(inst, Webhooker))
|
||||||
|
assert.False(s.T(), HasSupport(inst, "not_exist"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CompatSuite) TestSetConfig() {
|
||||||
|
inst := s.p.NewPluginInstance(UserContext{
|
||||||
|
ID: 3,
|
||||||
|
Name: "test3",
|
||||||
|
})
|
||||||
|
|
||||||
|
defaultConfig := inst.DefaultConfig()
|
||||||
|
assert.Nil(s.T(), inst.ValidateAndSetConfig(defaultConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CompatSuite) TestRegisterWebhook() {
|
||||||
|
inst := s.p.NewPluginInstance(UserContext{
|
||||||
|
ID: 4,
|
||||||
|
Name: "test4",
|
||||||
|
})
|
||||||
|
|
||||||
|
e := gin.New()
|
||||||
|
g := e.Group("/")
|
||||||
|
assert.NotPanics(s.T(), func() {
|
||||||
|
inst.RegisterWebhook("/plugin/4/custom/Pabcd/", g)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
func (s *CompatSuite) TestEnableDisable() {
|
||||||
|
inst := s.p.NewPluginInstance(UserContext{
|
||||||
|
ID: 5,
|
||||||
|
Name: "test5",
|
||||||
|
})
|
||||||
|
assert.Nil(s.T(), inst.Enable())
|
||||||
|
assert.Nil(s.T(), inst.Disable())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CompatSuite) TestGetDisplay() {
|
||||||
|
inst := s.p.NewPluginInstance(UserContext{
|
||||||
|
ID: 6,
|
||||||
|
Name: "test6",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NotEqual(s.T(), "", inst.GetDisplay(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompatSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(CompatSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapIncompatiblePlugins(t *testing.T) {
|
||||||
|
tmpDir := test.NewTmpDir("gotify_testwrapincompatibleplugins")
|
||||||
|
defer tmpDir.Clean()
|
||||||
|
for i, modulePath := range []string{
|
||||||
|
"github.com/gotify/server/plugin/testing/broken/noinstance",
|
||||||
|
"github.com/gotify/server/plugin/testing/broken/nothing",
|
||||||
|
"github.com/gotify/server/plugin/testing/broken/unknowninfo",
|
||||||
|
"github.com/gotify/server/plugin/testing/broken/malformedconstructor",
|
||||||
|
} {
|
||||||
|
fName := tmpDir.Path(fmt.Sprintf("broken_%d.so", i))
|
||||||
|
exec.Command("go", "get", "-d").Run()
|
||||||
|
goBuildFlags := []string{"build", "-buildmode=plugin", "-o=" + fName}
|
||||||
|
|
||||||
|
for _, extraFlag := range extraGoBuildFlags {
|
||||||
|
goBuildFlags = append(goBuildFlags, extraFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
goBuildFlags = append(goBuildFlags, modulePath)
|
||||||
|
|
||||||
|
cmd := exec.Command("go", goBuildFlags...)
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
assert.Nil(t, cmd.Run())
|
||||||
|
|
||||||
|
plugin, err := plugin.Open(fName)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
_, err = Wrap(plugin)
|
||||||
|
assert.Error(t, err)
|
||||||
|
os.Remove(fName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
// +build !race
|
||||||
|
|
||||||
|
package compat
|
||||||
|
|
||||||
|
var extraGoBuildFlags = []string{}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
// +build race
|
||||||
|
|
||||||
|
package compat
|
||||||
|
|
||||||
|
var extraGoBuildFlags = []string{"-race"}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gotify/plugin-api"
|
||||||
|
"github.com/robfig/cron"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetGotifyPluginInfo returns gotify plugin info
|
||||||
|
func GetGotifyPluginInfo() plugin.Info {
|
||||||
|
return plugin.Info{
|
||||||
|
Name: "clock",
|
||||||
|
Description: "Sends an hourly reminder",
|
||||||
|
ModulePath: "github.com/gotify/server/example/clock",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin is plugin instance
|
||||||
|
type Plugin struct {
|
||||||
|
msgHandler plugin.MessageHandler
|
||||||
|
enabled bool
|
||||||
|
cronHandler *cron.Cron
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable implements plugin.Plugin
|
||||||
|
func (c *Plugin) Enable() error {
|
||||||
|
c.enabled = true
|
||||||
|
c.cronHandler = cron.New()
|
||||||
|
c.cronHandler.AddFunc("0 0 * * *", func() {
|
||||||
|
c.msgHandler.SendMessage(plugin.Message{
|
||||||
|
Title: "Tick Tock!",
|
||||||
|
Message: time.Now().Format("It is 15:04:05 now."),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
c.cronHandler.Start()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable implements plugin.Plugin
|
||||||
|
func (c *Plugin) Disable() error {
|
||||||
|
if c.cronHandler != nil {
|
||||||
|
c.cronHandler.Stop()
|
||||||
|
}
|
||||||
|
c.enabled = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMessageHandler implements plugin.Messenger.
|
||||||
|
func (c *Plugin) SetMessageHandler(h plugin.MessageHandler) {
|
||||||
|
c.msgHandler = h
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGotifyPluginInstance creates a plugin instance for a user context.
|
||||||
|
func NewGotifyPluginInstance(ctx plugin.UserContext) plugin.Plugin {
|
||||||
|
p := &Plugin{}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
panic("this should be built as go plugin")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gotify/plugin-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetGotifyPluginInfo returns gotify plugin info.
|
||||||
|
func GetGotifyPluginInfo() plugin.Info {
|
||||||
|
return plugin.Info{
|
||||||
|
ModulePath: "github.com/gotify/server/plugin/example/echo",
|
||||||
|
Name: "test plugin",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EchoPlugin is the gotify plugin instance.
|
||||||
|
type EchoPlugin struct {
|
||||||
|
msgHandler plugin.MessageHandler
|
||||||
|
storageHandler plugin.StorageHandler
|
||||||
|
config *Config
|
||||||
|
basePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStorageHandler implements plugin.Storager
|
||||||
|
func (c *EchoPlugin) SetStorageHandler(h plugin.StorageHandler) {
|
||||||
|
c.storageHandler = h
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMessageHandler implements plugin.Messenger.
|
||||||
|
func (c *EchoPlugin) SetMessageHandler(h plugin.MessageHandler) {
|
||||||
|
c.msgHandler = h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage defines the plugin storage scheme
|
||||||
|
type Storage struct {
|
||||||
|
CalledTimes int `json:"called_times"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config defines the plugin config scheme
|
||||||
|
type Config struct {
|
||||||
|
MagicString string `yaml:"magic_string"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig implements plugin.Configurer
|
||||||
|
func (c *EchoPlugin) DefaultConfig() interface{} {
|
||||||
|
return &Config{
|
||||||
|
MagicString: "hello world",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAndSetConfig implements plugin.Configurer
|
||||||
|
func (c *EchoPlugin) ValidateAndSetConfig(config interface{}) error {
|
||||||
|
c.config = config.(*Config)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable enables the plugin.
|
||||||
|
func (c *EchoPlugin) Enable() error {
|
||||||
|
log.Println("echo plugin enabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable disables the plugin.
|
||||||
|
func (c *EchoPlugin) Disable() error {
|
||||||
|
log.Println("echo plugin disbled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterWebhook implements plugin.Webhooker.
|
||||||
|
func (c *EchoPlugin) RegisterWebhook(baseURL string, g *gin.RouterGroup) {
|
||||||
|
c.basePath = baseURL
|
||||||
|
g.GET("/echo", func(ctx *gin.Context) {
|
||||||
|
|
||||||
|
storage, _ := c.storageHandler.Load()
|
||||||
|
conf := new(Storage)
|
||||||
|
json.Unmarshal(storage, conf)
|
||||||
|
conf.CalledTimes++
|
||||||
|
newStorage, _ := json.Marshal(conf)
|
||||||
|
c.storageHandler.Save(newStorage)
|
||||||
|
|
||||||
|
c.msgHandler.SendMessage(plugin.Message{
|
||||||
|
Title: "Hello received",
|
||||||
|
Message: fmt.Sprintf("echo server received a hello message %d times", conf.CalledTimes),
|
||||||
|
Priority: 2,
|
||||||
|
Extras: map[string]interface{}{
|
||||||
|
"plugin::name": "echo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.Writer.WriteString(fmt.Sprintf("Magic string is: %s\r\nEcho server running at %secho", c.config.MagicString, c.basePath))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDisplay implements plugin.Displayer.
|
||||||
|
func (c *EchoPlugin) GetDisplay(location *url.URL) string {
|
||||||
|
loc := &url.URL{
|
||||||
|
Path: c.basePath,
|
||||||
|
}
|
||||||
|
if location != nil {
|
||||||
|
loc.Scheme = location.Scheme
|
||||||
|
loc.Host = location.Host
|
||||||
|
}
|
||||||
|
loc = loc.ResolveReference(&url.URL{
|
||||||
|
Path: "echo",
|
||||||
|
})
|
||||||
|
return "Echo plugin running at: " + loc.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGotifyPluginInstance creates a plugin instance for a user context.
|
||||||
|
func NewGotifyPluginInstance(ctx plugin.UserContext) plugin.Plugin {
|
||||||
|
return &EchoPlugin{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
panic("this should be built as go plugin")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gotify/plugin-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetGotifyPluginInfo returns gotify plugin info
|
||||||
|
func GetGotifyPluginInfo() plugin.Info {
|
||||||
|
return plugin.Info{
|
||||||
|
Name: "minimal plugin",
|
||||||
|
ModulePath: "github.com/gotify/server/example/minimal",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin is plugin instance
|
||||||
|
type Plugin struct{}
|
||||||
|
|
||||||
|
// Enable implements plugin.Plugin
|
||||||
|
func (c *Plugin) Enable() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable implements plugin.Plugin
|
||||||
|
func (c *Plugin) Disable() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGotifyPluginInstance creates a plugin instance for a user context.
|
||||||
|
func NewGotifyPluginInstance(ctx plugin.UserContext) plugin.Plugin {
|
||||||
|
return &Plugin{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
panic("this should be built as go plugin")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,382 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
"plugin"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-yaml/yaml"
|
||||||
|
"github.com/gotify/server/auth"
|
||||||
|
"github.com/gotify/server/model"
|
||||||
|
"github.com/gotify/server/plugin/compat"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The Database interface for encapsulating database access.
|
||||||
|
type Database interface {
|
||||||
|
GetUsers() []*model.User
|
||||||
|
GetPluginConfByUserAndPath(userid uint, path string) *model.PluginConf
|
||||||
|
CreatePluginConf(p *model.PluginConf) error
|
||||||
|
GetPluginConfByApplicationID(appid uint) *model.PluginConf
|
||||||
|
UpdatePluginConf(p *model.PluginConf) error
|
||||||
|
CreateMessage(message *model.Message) error
|
||||||
|
GetPluginConfByID(id uint) *model.PluginConf
|
||||||
|
GetPluginConfByToken(token string) *model.PluginConf
|
||||||
|
GetUserByID(id uint) *model.User
|
||||||
|
CreateApplication(application *model.Application) error
|
||||||
|
UpdateApplication(app *model.Application) error
|
||||||
|
GetApplicationsByUser(userID uint) []*model.Application
|
||||||
|
GetApplicationByToken(token string) *model.Application
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notifier notifies when a new message was created.
|
||||||
|
type Notifier interface {
|
||||||
|
Notify(userID uint, message *model.MessageExternal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manager is an encapusulating layer for plugins and manages all plugins and its instances.
|
||||||
|
type Manager struct {
|
||||||
|
mutex *sync.RWMutex
|
||||||
|
instances map[uint]compat.PluginInstance
|
||||||
|
plugins map[string]compat.Plugin
|
||||||
|
messages chan MessageWithUserID
|
||||||
|
db Database
|
||||||
|
mux *gin.RouterGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager created a Manager from configurations.
|
||||||
|
func NewManager(db Database, directory string, mux *gin.RouterGroup, notifier Notifier) (*Manager, error) {
|
||||||
|
manager := &Manager{
|
||||||
|
mutex: &sync.RWMutex{},
|
||||||
|
instances: map[uint]compat.PluginInstance{},
|
||||||
|
plugins: map[string]compat.Plugin{},
|
||||||
|
messages: make(chan MessageWithUserID),
|
||||||
|
db: db,
|
||||||
|
mux: mux,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
message := <-manager.messages
|
||||||
|
internalMsg := &model.Message{
|
||||||
|
ApplicationID: message.Message.ApplicationID,
|
||||||
|
Title: message.Message.Title,
|
||||||
|
Priority: message.Message.Priority,
|
||||||
|
Date: message.Message.Date,
|
||||||
|
Message: message.Message.Message,
|
||||||
|
}
|
||||||
|
if message.Message.Extras != nil {
|
||||||
|
internalMsg.Extras, _ = json.Marshal(message.Message.Extras)
|
||||||
|
}
|
||||||
|
db.CreateMessage(internalMsg)
|
||||||
|
message.Message.ID = internalMsg.ID
|
||||||
|
notifier.Notify(message.UserID, &message.Message)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := manager.loadPlugins(directory); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range manager.db.GetUsers() {
|
||||||
|
if err := manager.initializeForUser(*user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return manager, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrAlreadyEnabledOrDisabled is returned on SetPluginEnabled call when a plugin is already enabled or disabled
|
||||||
|
var ErrAlreadyEnabledOrDisabled = errors.New("config is already enabled/disabled")
|
||||||
|
|
||||||
|
func (m *Manager) applicationExists(token string) bool {
|
||||||
|
return m.db.GetApplicationByToken(token) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) pluginConfExists(token string) bool {
|
||||||
|
return m.db.GetPluginConfByToken(token) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPluginEnabled sets the plugins enabled state.
|
||||||
|
func (m *Manager) SetPluginEnabled(pluginID uint, enabled bool) error {
|
||||||
|
instance, err := m.Instance(pluginID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("instance not found")
|
||||||
|
}
|
||||||
|
conf := m.db.GetPluginConfByID(pluginID)
|
||||||
|
|
||||||
|
if conf.Enabled == enabled {
|
||||||
|
return ErrAlreadyEnabledOrDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
err = instance.Enable()
|
||||||
|
} else {
|
||||||
|
err = instance.Disable()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conf.Enabled = enabled
|
||||||
|
return m.db.UpdatePluginConf(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginInfo returns plugin info.
|
||||||
|
func (m *Manager) PluginInfo(modulePath string) compat.Info {
|
||||||
|
m.mutex.RLock()
|
||||||
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
|
if p, ok := m.plugins[modulePath]; ok {
|
||||||
|
return p.PluginInfo()
|
||||||
|
}
|
||||||
|
fmt.Println("Could not get plugin info for", modulePath)
|
||||||
|
return compat.Info{
|
||||||
|
Name: "UNKNOWN",
|
||||||
|
ModulePath: modulePath,
|
||||||
|
Description: "Oops something went wrong",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance returns an instance with the given ID
|
||||||
|
func (m *Manager) Instance(pluginID uint) (compat.PluginInstance, error) {
|
||||||
|
m.mutex.RLock()
|
||||||
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
|
if instance, ok := m.instances[pluginID]; ok {
|
||||||
|
return instance, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("instance not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasInstance returns whether the given plugin ID has a corresponding instance
|
||||||
|
func (m *Manager) HasInstance(pluginID uint) bool {
|
||||||
|
instance, err := m.Instance(pluginID)
|
||||||
|
return err == nil && instance != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveUser disabled all plugins of a user when the user is disabled
|
||||||
|
func (m *Manager) RemoveUser(userID uint) error {
|
||||||
|
for _, p := range m.plugins {
|
||||||
|
pluginConf := m.db.GetPluginConfByUserAndPath(userID, p.PluginInfo().ModulePath)
|
||||||
|
if pluginConf.Enabled {
|
||||||
|
inst, err := m.Instance(pluginConf.ID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.mutex.Lock()
|
||||||
|
err = inst.Disable()
|
||||||
|
m.mutex.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(m.instances, pluginConf.ID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type pluginFileLoadError struct {
|
||||||
|
Filename string
|
||||||
|
UnderlyingError error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c pluginFileLoadError) Error() string {
|
||||||
|
return fmt.Sprintf("error while loading plugin %s: %s", c.Filename, c.UnderlyingError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) loadPlugins(directory string) error {
|
||||||
|
if directory == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginFiles, err := ioutil.ReadDir(directory)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while reading directory %s", err)
|
||||||
|
}
|
||||||
|
for _, f := range pluginFiles {
|
||||||
|
pluginPath := filepath.Join(directory, "./", f.Name())
|
||||||
|
pRaw, err := plugin.Open(pluginPath)
|
||||||
|
if err != nil {
|
||||||
|
return pluginFileLoadError{f.Name(), err}
|
||||||
|
}
|
||||||
|
compatPlugin, err := compat.Wrap(pRaw)
|
||||||
|
if err != nil {
|
||||||
|
return pluginFileLoadError{f.Name(), err}
|
||||||
|
}
|
||||||
|
if err := m.LoadPlugin(compatPlugin); err != nil {
|
||||||
|
return pluginFileLoadError{f.Name(), err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPlugin loads a compat plugin, exported to sideload plugins for testing purposes
|
||||||
|
func (m *Manager) LoadPlugin(compatPlugin compat.Plugin) error {
|
||||||
|
modulePath := compatPlugin.PluginInfo().ModulePath
|
||||||
|
if _, ok := m.plugins[modulePath]; ok {
|
||||||
|
return fmt.Errorf("plugin with module path %s is present at least twice", modulePath)
|
||||||
|
}
|
||||||
|
m.plugins[modulePath] = compatPlugin
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitializeForUserID initializes all plugin instances for a given user
|
||||||
|
func (m *Manager) InitializeForUserID(userID uint) error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
user := m.db.GetUserByID(userID)
|
||||||
|
if user != nil {
|
||||||
|
return m.initializeForUser(*user)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("user with id %d not found", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) initializeForUser(user model.User) error {
|
||||||
|
|
||||||
|
userCtx := compat.UserContext{
|
||||||
|
ID: user.ID,
|
||||||
|
Name: user.Name,
|
||||||
|
Admin: user.Admin,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range m.plugins {
|
||||||
|
if err := m.initializeSingleUserPlugin(userCtx, p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, app := range m.db.GetApplicationsByUser(user.ID) {
|
||||||
|
if conf := m.db.GetPluginConfByApplicationID(app.ID); conf != nil {
|
||||||
|
_, compatExist := m.plugins[conf.ModulePath]
|
||||||
|
app.Internal = compatExist
|
||||||
|
} else {
|
||||||
|
app.Internal = false
|
||||||
|
}
|
||||||
|
m.db.UpdateApplication(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) initializeSingleUserPlugin(userCtx compat.UserContext, p compat.Plugin) error {
|
||||||
|
info := p.PluginInfo()
|
||||||
|
instance := p.NewPluginInstance(userCtx)
|
||||||
|
userID := userCtx.ID
|
||||||
|
|
||||||
|
pluginConf := m.db.GetPluginConfByUserAndPath(userID, info.ModulePath)
|
||||||
|
|
||||||
|
if pluginConf == nil {
|
||||||
|
var err error
|
||||||
|
pluginConf, err = m.createPluginConf(instance, info, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.instances[pluginConf.ID] = instance
|
||||||
|
|
||||||
|
if compat.HasSupport(instance, compat.Messenger) {
|
||||||
|
instance.SetMessageHandler(redirectToChannel{
|
||||||
|
ApplicationID: pluginConf.ApplicationID,
|
||||||
|
UserID: pluginConf.UserID,
|
||||||
|
Messages: m.messages})
|
||||||
|
}
|
||||||
|
if compat.HasSupport(instance, compat.Storager) {
|
||||||
|
instance.SetStorageHandler(dbStorageHandler{pluginConf.ID, m.db})
|
||||||
|
}
|
||||||
|
if compat.HasSupport(instance, compat.Configurer) {
|
||||||
|
m.initializeConfigurerForSingleUserPlugin(instance, pluginConf)
|
||||||
|
}
|
||||||
|
if compat.HasSupport(instance, compat.Webhooker) {
|
||||||
|
id := pluginConf.ID
|
||||||
|
g := m.mux.Group(pluginConf.Token+"/", requirePluginEnabled(id, m.db))
|
||||||
|
instance.RegisterWebhook(strings.Replace(g.BasePath(), ":id", strconv.Itoa(int(id)), 1), g)
|
||||||
|
}
|
||||||
|
if pluginConf.Enabled {
|
||||||
|
err := instance.Enable()
|
||||||
|
if err != nil {
|
||||||
|
// Single user plugin cannot be enabled
|
||||||
|
// Don't panic, disable for now and wait for user to update config
|
||||||
|
log.Printf("Plugin initialize failed for user %s: %s. Disabling now...", userCtx.Name, err.Error())
|
||||||
|
pluginConf.Enabled = false
|
||||||
|
m.db.UpdatePluginConf(pluginConf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) initializeConfigurerForSingleUserPlugin(instance compat.PluginInstance, pluginConf *model.PluginConf) {
|
||||||
|
if len(pluginConf.Config) == 0 {
|
||||||
|
// The Configurer is newly implemented
|
||||||
|
// Use the default config
|
||||||
|
pluginConf.Config, _ = yaml.Marshal(instance.DefaultConfig())
|
||||||
|
m.db.UpdatePluginConf(pluginConf)
|
||||||
|
}
|
||||||
|
c := instance.DefaultConfig()
|
||||||
|
if yaml.Unmarshal(pluginConf.Config, c) != nil || instance.ValidateAndSetConfig(c) != nil {
|
||||||
|
pluginConf.Enabled = false
|
||||||
|
|
||||||
|
log.Printf("Plugin %s for user %d failed to initialize because it rejected the current config. It might be outdated. A default config is used and the user would need to enable it again.", pluginConf.ModulePath, pluginConf.UserID)
|
||||||
|
newConf := bytes.NewBufferString("# Plugin initialization failed because it rejected the current config. It might be outdated.\r\n# A default plugin configuration is used:\r\n")
|
||||||
|
|
||||||
|
d, _ := yaml.Marshal(c)
|
||||||
|
newConf.Write(d)
|
||||||
|
newConf.WriteString("\r\n")
|
||||||
|
|
||||||
|
newConf.WriteString("# The original configuration: \r\n")
|
||||||
|
oldConf := bufio.NewScanner(bytes.NewReader(pluginConf.Config))
|
||||||
|
for oldConf.Scan() {
|
||||||
|
newConf.WriteString("# ")
|
||||||
|
newConf.WriteString(oldConf.Text())
|
||||||
|
newConf.WriteString("\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginConf.Config = newConf.Bytes()
|
||||||
|
|
||||||
|
m.db.UpdatePluginConf(pluginConf)
|
||||||
|
instance.ValidateAndSetConfig(instance.DefaultConfig())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) createPluginConf(instance compat.PluginInstance, info compat.Info, userID uint) (*model.PluginConf, error) {
|
||||||
|
pluginConf := &model.PluginConf{
|
||||||
|
UserID: userID,
|
||||||
|
ModulePath: info.ModulePath,
|
||||||
|
Token: auth.GenerateNotExistingToken(auth.GeneratePluginToken, m.pluginConfExists),
|
||||||
|
}
|
||||||
|
if compat.HasSupport(instance, compat.Configurer) {
|
||||||
|
pluginConf.Config, _ = yaml.Marshal(instance.DefaultConfig())
|
||||||
|
}
|
||||||
|
if compat.HasSupport(instance, compat.Messenger) {
|
||||||
|
app := &model.Application{
|
||||||
|
Token: auth.GenerateNotExistingToken(auth.GenerateApplicationToken, m.applicationExists),
|
||||||
|
Name: info.String(),
|
||||||
|
UserID: userID,
|
||||||
|
Internal: true,
|
||||||
|
Description: fmt.Sprintf("auto generated application for %s", info.ModulePath),
|
||||||
|
}
|
||||||
|
if err := m.db.CreateApplication(app); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pluginConf.ApplicationID = app.ID
|
||||||
|
|
||||||
|
}
|
||||||
|
if err := m.db.CreatePluginConf(pluginConf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pluginConf, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,452 @@
|
||||||
|
// +build linux darwin
|
||||||
|
// +build !race
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gotify/server/auth"
|
||||||
|
"github.com/gotify/server/model"
|
||||||
|
"github.com/gotify/server/plugin/compat"
|
||||||
|
"github.com/gotify/server/plugin/testing/mock"
|
||||||
|
"github.com/gotify/server/test"
|
||||||
|
"github.com/gotify/server/test/testdb"
|
||||||
|
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const examplePluginPath = "github.com/gotify/server/plugin/example/echo"
|
||||||
|
const mockPluginPath = mock.ModulePath
|
||||||
|
const danglingPluginPath = "github.com/gotify/server/plugin/testing/removed"
|
||||||
|
|
||||||
|
type ManagerSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
db *testdb.Database
|
||||||
|
manager *Manager
|
||||||
|
e *gin.Engine
|
||||||
|
g *gin.RouterGroup
|
||||||
|
msgReceiver chan MessageWithUserID
|
||||||
|
|
||||||
|
tmpDir test.TmpDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) Notify(uid uint, message *model.MessageExternal) {
|
||||||
|
s.msgReceiver <- MessageWithUserID{
|
||||||
|
Message: *message,
|
||||||
|
UserID: uid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) SetupSuite() {
|
||||||
|
s.tmpDir = test.NewTmpDir("gotify_managersuite")
|
||||||
|
|
||||||
|
test.WithWd(path.Join(test.GetProjectDir(), "./plugin/example/echo"), func(origWd string) {
|
||||||
|
exec.Command("go", "get", "-d").Run()
|
||||||
|
goBuildFlags := []string{"build", "-buildmode=plugin", "-o=" + s.tmpDir.Path("echo.so")}
|
||||||
|
|
||||||
|
for _, extraFlag := range extraGoBuildFlags {
|
||||||
|
goBuildFlags = append(goBuildFlags, extraFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("go", goBuildFlags...)
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
assert.Nil(s.T(), cmd.Run())
|
||||||
|
})
|
||||||
|
|
||||||
|
s.db = testdb.NewDBWithDefaultUser(s.T())
|
||||||
|
s.makeDanglingPluginConf(1)
|
||||||
|
|
||||||
|
e := gin.New()
|
||||||
|
manager, err := NewManager(s.db.GormDatabase, s.tmpDir.Path(), e.Group("/plugin/:id/custom/"), s)
|
||||||
|
s.e = e
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
p := new(mock.Plugin)
|
||||||
|
assert.Nil(s.T(), manager.LoadPlugin(p))
|
||||||
|
assert.Nil(s.T(), manager.initializeSingleUserPlugin(compat.UserContext{
|
||||||
|
ID: 1,
|
||||||
|
Admin: true,
|
||||||
|
}, p))
|
||||||
|
|
||||||
|
s.manager = manager
|
||||||
|
s.msgReceiver = make(chan MessageWithUserID)
|
||||||
|
|
||||||
|
assert.Contains(s.T(), s.manager.plugins, examplePluginPath)
|
||||||
|
assert.NotNil(s.T(), s.db.GetPluginConfByUserAndPath(1, examplePluginPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TearDownSuite() {
|
||||||
|
assert.Nil(s.T(), s.tmpDir.Clean())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) getConfForExamplePlugin(uid uint) *model.PluginConf {
|
||||||
|
return s.db.GetPluginConfByUserAndPath(uid, examplePluginPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) getConfForMockPlugin(uid uint) *model.PluginConf {
|
||||||
|
return s.db.GetPluginConfByUserAndPath(uid, mockPluginPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) getMockPluginInstance(uid uint) *mock.PluginInstance {
|
||||||
|
pid := s.getConfForMockPlugin(uid).ID
|
||||||
|
return s.manager.instances[pid].(*mock.PluginInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) makeDanglingPluginConf(uid uint) *model.PluginConf {
|
||||||
|
conf := &model.PluginConf{
|
||||||
|
UserID: uid,
|
||||||
|
ModulePath: danglingPluginPath,
|
||||||
|
Token: auth.GeneratePluginToken(),
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
s.db.CreatePluginConf(conf)
|
||||||
|
return conf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestWebhook_blockedIfDisabled() {
|
||||||
|
conf := s.getConfForExamplePlugin(1)
|
||||||
|
t := httptest.NewRequest("GET", fmt.Sprintf("/plugin/%d/custom/%s/echo", conf.ID, conf.Token), nil)
|
||||||
|
|
||||||
|
r := httptest.NewRecorder()
|
||||||
|
s.e.ServeHTTP(r, t)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 400, r.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestWebhook_successIfEnabled() {
|
||||||
|
conf := s.getConfForExamplePlugin(1)
|
||||||
|
|
||||||
|
assert.Nil(s.T(), s.manager.SetPluginEnabled(conf.ID, true))
|
||||||
|
defer func() { assert.Nil(s.T(), s.manager.SetPluginEnabled(conf.ID, false)) }()
|
||||||
|
assert.True(s.T(), s.getConfForExamplePlugin(1).Enabled)
|
||||||
|
|
||||||
|
t := httptest.NewRequest("GET", fmt.Sprintf("/plugin/%d/custom/%s/echo", conf.ID, conf.Token), nil)
|
||||||
|
|
||||||
|
r := httptest.NewRecorder()
|
||||||
|
s.e.ServeHTTP(r, t)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, r.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestInitializePlugin_noOpIfEmpty() {
|
||||||
|
assert.Nil(s.T(), s.manager.loadPlugins(""))
|
||||||
|
}
|
||||||
|
func (s *ManagerSuite) TestInitializePlugin_directoryInvalid_expectError() {
|
||||||
|
assert.Error(s.T(), s.manager.loadPlugins("<<"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestInitializePlugin_invalidPlugin_expectError() {
|
||||||
|
assert.Error(s.T(), s.manager.loadPlugins(test.GetProjectDir()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestInitializePlugin_brokenPlugin_expectError() {
|
||||||
|
tmpDir := test.NewTmpDir("gotify_testbrokenplugin")
|
||||||
|
defer tmpDir.Clean()
|
||||||
|
test.WithWd(path.Join(test.GetProjectDir(), "./plugin/testing/broken/nothing"), func(origWd string) {
|
||||||
|
exec.Command("go", "get", "-d").Run()
|
||||||
|
goBuildFlags := []string{"build", "-buildmode=plugin", "-o=" + tmpDir.Path("empty.so")}
|
||||||
|
|
||||||
|
for _, extraFlag := range extraGoBuildFlags {
|
||||||
|
goBuildFlags = append(goBuildFlags, extraFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("go", goBuildFlags...)
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
assert.Nil(s.T(), cmd.Run())
|
||||||
|
})
|
||||||
|
assert.Error(s.T(), s.manager.loadPlugins(tmpDir.Path()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestInitializePlugin_alreadyLoaded_expectError() {
|
||||||
|
assert.Error(s.T(), s.manager.loadPlugins(s.tmpDir.Path()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestInitializePlugin_alreadyEnabledInConf_expectAutoEnable() {
|
||||||
|
s.db.User(2)
|
||||||
|
s.db.CreatePluginConf(&model.PluginConf{
|
||||||
|
UserID: 2,
|
||||||
|
ModulePath: mockPluginPath,
|
||||||
|
Token: "P1234",
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Nil(s.T(), s.manager.InitializeForUserID(2))
|
||||||
|
inst := s.getMockPluginInstance(2)
|
||||||
|
assert.True(s.T(), inst.Enabled)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestInitializePlugin_alreadyEnabledInConf_failedToLoadConfig_disableAutomatically() {
|
||||||
|
s.db.User(3)
|
||||||
|
s.db.CreatePluginConf(&model.PluginConf{
|
||||||
|
UserID: 3,
|
||||||
|
ModulePath: mockPluginPath,
|
||||||
|
Token: "Ptttt",
|
||||||
|
Enabled: true,
|
||||||
|
Config: []byte(`invalid: """`),
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Nil(s.T(), s.manager.InitializeForUserID(3))
|
||||||
|
inst := s.getMockPluginInstance(3)
|
||||||
|
assert.False(s.T(), inst.Enabled)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestInitializePlugin_alreadyEnabled_cannotEnable_disabledAutomatically() {
|
||||||
|
s.db.NewUserWithName(4, "enable_fail_2")
|
||||||
|
mock.ReturnErrorOnEnableForUser(4, errors.New("test error"))
|
||||||
|
s.db.CreatePluginConf(&model.PluginConf{
|
||||||
|
UserID: 4,
|
||||||
|
ModulePath: mockPluginPath,
|
||||||
|
Token: "P5478",
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Nil(s.T(), s.manager.InitializeForUserID(4))
|
||||||
|
inst := s.getMockPluginInstance(4)
|
||||||
|
assert.False(s.T(), inst.Enabled)
|
||||||
|
assert.False(s.T(), s.getConfForMockPlugin(4).Enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestInitializePlugin_userIDNotExist_expectError() {
|
||||||
|
assert.Error(s.T(), s.manager.InitializeForUserID(99))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestSetPluginEnabled() {
|
||||||
|
pid := s.getConfForMockPlugin(1).ID
|
||||||
|
assert.Nil(s.T(), s.manager.SetPluginEnabled(pid, true))
|
||||||
|
assert.Error(s.T(), s.manager.SetPluginEnabled(pid, true))
|
||||||
|
assert.Nil(s.T(), s.manager.SetPluginEnabled(pid, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestSetPluginEnabled_EnableReturnsError_cannotEnable() {
|
||||||
|
s.db.NewUserWithName(5, "enable_fail")
|
||||||
|
errExpected := errors.New("test error")
|
||||||
|
mock.ReturnErrorOnEnableForUser(5, errExpected)
|
||||||
|
|
||||||
|
assert.Nil(s.T(), s.manager.InitializeForUserID(5))
|
||||||
|
|
||||||
|
pid := s.getConfForMockPlugin(5).ID
|
||||||
|
assert.Error(s.T(), s.manager.SetPluginEnabled(pid, false))
|
||||||
|
assert.EqualError(s.T(), s.manager.SetPluginEnabled(pid, true), errExpected.Error())
|
||||||
|
|
||||||
|
assert.False(s.T(), s.getConfForMockPlugin(5).Enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestSetPluginEnabled_DisableReturnsError_cannotDisable() {
|
||||||
|
s.db.NewUserWithName(6, "disable_fail")
|
||||||
|
errExpected := errors.New("test error")
|
||||||
|
mock.ReturnErrorOnDisableForUser(6, errExpected)
|
||||||
|
|
||||||
|
assert.Nil(s.T(), s.manager.InitializeForUserID(6))
|
||||||
|
|
||||||
|
pid := s.getConfForMockPlugin(6).ID
|
||||||
|
assert.Nil(s.T(), s.manager.SetPluginEnabled(pid, true))
|
||||||
|
assert.EqualError(s.T(), s.manager.SetPluginEnabled(pid, false), errExpected.Error())
|
||||||
|
|
||||||
|
assert.True(s.T(), s.getConfForMockPlugin(6).Enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestAddRemoveNewUser() {
|
||||||
|
s.db.User(7)
|
||||||
|
s.makeDanglingPluginConf(7)
|
||||||
|
|
||||||
|
assert.Nil(s.T(), s.manager.InitializeForUserID(7))
|
||||||
|
pid := s.getConfForExamplePlugin(7).ID
|
||||||
|
assert.True(s.T(), s.manager.HasInstance(pid))
|
||||||
|
|
||||||
|
assert.Nil(s.T(), s.manager.SetPluginEnabled(s.getConfForMockPlugin(7).ID, true))
|
||||||
|
|
||||||
|
assert.Nil(s.T(), s.manager.RemoveUser(7))
|
||||||
|
assert.False(s.T(), s.manager.HasInstance(pid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestRemoveUser_DisableFail_cannotRemove() {
|
||||||
|
s.manager.initializeForUser(*s.db.NewUserWithName(8, "disable_fail_2"))
|
||||||
|
errExpected := errors.New("test error")
|
||||||
|
mock.ReturnErrorOnDisableForUser(8, errExpected)
|
||||||
|
s.manager.SetPluginEnabled(s.getConfForMockPlugin(8).ID, true)
|
||||||
|
|
||||||
|
assert.EqualError(s.T(), s.manager.RemoveUser(8), errExpected.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestRemoveUser_danglingConf_expectSuccess() {
|
||||||
|
// make a dangling conf for this instance
|
||||||
|
s.db.User(9)
|
||||||
|
s.db.CreatePluginConf(&model.PluginConf{
|
||||||
|
ModulePath: mockPluginPath,
|
||||||
|
Enabled: true,
|
||||||
|
UserID: 9,
|
||||||
|
Token: auth.GenerateNotExistingToken(auth.GeneratePluginToken, s.manager.pluginConfExists),
|
||||||
|
})
|
||||||
|
s.db.CreatePluginConf(&model.PluginConf{
|
||||||
|
ModulePath: examplePluginPath,
|
||||||
|
Enabled: true,
|
||||||
|
UserID: 9,
|
||||||
|
Token: auth.GenerateNotExistingToken(auth.GeneratePluginToken, s.manager.pluginConfExists),
|
||||||
|
})
|
||||||
|
assert.Nil(s.T(), s.manager.RemoveUser(9))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestTriggerMessage() {
|
||||||
|
inst := s.getMockPluginInstance(1)
|
||||||
|
inst.TriggerMessage()
|
||||||
|
select {
|
||||||
|
case msg := <-s.msgReceiver:
|
||||||
|
assert.Equal(s.T(), uint(1), msg.UserID)
|
||||||
|
assert.NotEmpty(s.T(), msg.Message.Extras)
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
assert.Fail(s.T(), "read message time out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestStorage() {
|
||||||
|
inst := s.getMockPluginInstance(1)
|
||||||
|
|
||||||
|
assert.Nil(s.T(), inst.SetStorage([]byte("test")))
|
||||||
|
storage, err := inst.GetStorage()
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
assert.Equal(s.T(), "test", string(storage))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestGetPluginInfo() {
|
||||||
|
assert.Equal(s.T(), mock.Name, s.manager.PluginInfo(mock.ModulePath).Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestGetPluginInfo_notFound_doNotPanic() {
|
||||||
|
assert.NotPanics(s.T(), func() {
|
||||||
|
s.manager.PluginInfo("not/exist")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ManagerSuite) TestSetPluginEnabled_expectNotFound() {
|
||||||
|
assert.Error(s.T(), s.manager.SetPluginEnabled(99, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManagerSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(ManagerSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewManager_CannotLoadDirectory_expectError(t *testing.T) {
|
||||||
|
_, err := NewManager(nil, "<>", nil, nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewManager_NonPluginFile_expectError(t *testing.T) {
|
||||||
|
_, err := NewManager(nil, path.Join(test.GetProjectDir(), "test/assets/"), nil, nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewManager_FaultyDB_expectError(t *testing.T) {
|
||||||
|
tmpDir := test.NewTmpDir("gotify_testnewmanager_faultydb")
|
||||||
|
defer tmpDir.Clean()
|
||||||
|
for _, suite := range []struct {
|
||||||
|
pkg string
|
||||||
|
faultyTable string
|
||||||
|
name string
|
||||||
|
}{{"plugin/example/minimal/", "plugin_confs", "minimal"}, {"plugin/example/clock/", "applications", "clock"}} {
|
||||||
|
test.WithWd(path.Join(test.GetProjectDir(), suite.pkg), func(origWd string) {
|
||||||
|
exec.Command("go", "get", "-d").Run()
|
||||||
|
goBuildFlags := []string{"build", "-buildmode=plugin", "-o=" + tmpDir.Path(fmt.Sprintf("%s.so", suite.name))}
|
||||||
|
|
||||||
|
for _, extraFlag := range extraGoBuildFlags {
|
||||||
|
goBuildFlags = append(goBuildFlags, extraFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("go", goBuildFlags...)
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
assert.Nil(t, cmd.Run())
|
||||||
|
})
|
||||||
|
db := testdb.NewDBWithDefaultUser(t)
|
||||||
|
db.GormDatabase.DB.Callback().Create().Register("no_create", func(s *gorm.Scope) {
|
||||||
|
if s.TableName() == suite.faultyTable {
|
||||||
|
s.Err(errors.New("database failed"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
_, err := NewManager(db, tmpDir.Path(), nil, nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
os.Remove(tmpDir.Path(fmt.Sprintf("%s.so", suite.name)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewManager_InternalApplicationManagement(t *testing.T) {
|
||||||
|
db := testdb.NewDBWithDefaultUser(t)
|
||||||
|
|
||||||
|
{
|
||||||
|
// Application exist, no plugin conf
|
||||||
|
db.CreateApplication(&model.Application{
|
||||||
|
Token: "Ainternal_obsolete",
|
||||||
|
Internal: true,
|
||||||
|
Name: "obsolete plugin application",
|
||||||
|
UserID: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.True(t, db.GetApplicationByToken("Ainternal_obsolete").Internal)
|
||||||
|
_, err := NewManager(db, "", nil, nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.False(t, db.GetApplicationByToken("Ainternal_obsolete").Internal)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Application exist, conf exist, no compat
|
||||||
|
db.CreateApplication(&model.Application{
|
||||||
|
Token: "Ainternal_not_loaded",
|
||||||
|
Internal: true,
|
||||||
|
Name: "not loaded plugin application",
|
||||||
|
UserID: 1,
|
||||||
|
})
|
||||||
|
db.CreatePluginConf(&model.PluginConf{
|
||||||
|
ApplicationID: db.GetApplicationByToken("Ainternal_not_loaded").ID,
|
||||||
|
UserID: 1,
|
||||||
|
Enabled: true,
|
||||||
|
Token: auth.GeneratePluginToken(),
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.True(t, db.GetApplicationByToken("Ainternal_not_loaded").Internal)
|
||||||
|
_, err := NewManager(db, "", nil, nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.False(t, db.GetApplicationByToken("Ainternal_not_loaded").Internal)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Application exist, conf exist, has compat
|
||||||
|
db.CreateApplication(&model.Application{
|
||||||
|
Token: "Ainternal_loaded",
|
||||||
|
Internal: false,
|
||||||
|
Name: "not loaded plugin application",
|
||||||
|
UserID: 1,
|
||||||
|
})
|
||||||
|
db.CreatePluginConf(&model.PluginConf{
|
||||||
|
ApplicationID: db.GetApplicationByToken("Ainternal_loaded").ID,
|
||||||
|
UserID: 1,
|
||||||
|
Enabled: true,
|
||||||
|
ModulePath: mock.ModulePath,
|
||||||
|
Token: auth.GeneratePluginToken(),
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.False(t, db.GetApplicationByToken("Ainternal_loaded").Internal)
|
||||||
|
manager, err := NewManager(db, "", nil, nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Nil(t, manager.LoadPlugin(new(mock.Plugin)))
|
||||||
|
assert.Nil(t, manager.InitializeForUserID(1))
|
||||||
|
assert.True(t, db.GetApplicationByToken("Ainternal_loaded").Internal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginFileLoadError(t *testing.T) {
|
||||||
|
err := pluginFileLoadError{Filename: "test.so", UnderlyingError: errors.New("test error")}
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "test.so")
|
||||||
|
assert.Contains(t, err.Error(), "test error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
// +build !race
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
var extraGoBuildFlags = []string{}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
// +build race
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
var extraGoBuildFlags = []string{"-race"}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gotify/server/model"
|
||||||
|
"github.com/gotify/server/plugin/compat"
|
||||||
|
)
|
||||||
|
|
||||||
|
type redirectToChannel struct {
|
||||||
|
ApplicationID uint
|
||||||
|
UserID uint
|
||||||
|
Messages chan MessageWithUserID
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageWithUserID encapsulates a message with a given user ID
|
||||||
|
type MessageWithUserID struct {
|
||||||
|
Message model.MessageExternal
|
||||||
|
UserID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessage sends a message to the underlying message channel
|
||||||
|
func (c redirectToChannel) SendMessage(msg compat.Message) error {
|
||||||
|
c.Messages <- MessageWithUserID{
|
||||||
|
Message: model.MessageExternal{
|
||||||
|
ApplicationID: c.ApplicationID,
|
||||||
|
Message: msg.Message,
|
||||||
|
Title: msg.Title,
|
||||||
|
Priority: msg.Priority,
|
||||||
|
Date: time.Now(),
|
||||||
|
Extras: msg.Extras,
|
||||||
|
},
|
||||||
|
UserID: c.UserID,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func requirePluginEnabled(id uint, db Database) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if conf := db.GetPluginConfByID(id); conf == nil || !conf.Enabled {
|
||||||
|
c.AbortWithError(400, errors.New("plugin is disabled"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/gotify/server/model"
|
||||||
|
"github.com/gotify/server/test/testdb"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRequirePluginEnabled(t *testing.T) {
|
||||||
|
|
||||||
|
db := testdb.NewDBWithDefaultUser(t)
|
||||||
|
conf := &model.PluginConf{
|
||||||
|
ID: 1,
|
||||||
|
UserID: 1,
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
db.CreatePluginConf(conf)
|
||||||
|
|
||||||
|
g := gin.New()
|
||||||
|
|
||||||
|
mux := g.Group("/", requirePluginEnabled(1, db))
|
||||||
|
|
||||||
|
mux.GET("/", func(c *gin.Context) {
|
||||||
|
c.Status(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
getCode := func() int {
|
||||||
|
r := httptest.NewRequest("GET", "/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
g.ServeHTTP(w, r)
|
||||||
|
return w.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, 200, getCode())
|
||||||
|
|
||||||
|
conf.Enabled = false
|
||||||
|
db.UpdatePluginConf(conf)
|
||||||
|
assert.Equal(t, 400, getCode())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
type dbStorageHandler struct {
|
||||||
|
pluginID uint
|
||||||
|
db Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c dbStorageHandler) Save(b []byte) error {
|
||||||
|
conf := c.db.GetPluginConfByID(c.pluginID)
|
||||||
|
conf.Storage = b
|
||||||
|
return c.db.UpdatePluginConf(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c dbStorageHandler) Load() ([]byte, error) {
|
||||||
|
return c.db.GetPluginConfByID(c.pluginID).Storage, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/gotify/plugin-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetGotifyPluginInfo returns gotify plugin info
|
||||||
|
func GetGotifyPluginInfo() plugin.Info {
|
||||||
|
return plugin.Info{
|
||||||
|
ModulePath: "github.com/gotify/server/plugin/testing/broken/noinstance",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin is plugin instance
|
||||||
|
type Plugin struct{}
|
||||||
|
|
||||||
|
// Enable implements plugin.Plugin
|
||||||
|
func (c *Plugin) Enable() error {
|
||||||
|
return errors.New("cannot instantiate")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable implements plugin.Plugin
|
||||||
|
func (c *Plugin) Disable() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGotifyPluginInstance creates a plugin instance for a user context.
|
||||||
|
func NewGotifyPluginInstance(ctx plugin.UserContext) plugin.Plugin {
|
||||||
|
return &Plugin{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
panic("this is a broken plugin for testing purposes")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gotify/plugin-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetGotifyPluginInfo returns gotify plugin info
|
||||||
|
func GetGotifyPluginInfo() plugin.Info {
|
||||||
|
return plugin.Info{
|
||||||
|
ModulePath: "github.com/gotify/server/plugin/testing/broken/malformedconstructor",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin is plugin instance
|
||||||
|
type Plugin struct{}
|
||||||
|
|
||||||
|
// Enable implements plugin.Plugin
|
||||||
|
func (c *Plugin) Enable() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable implements plugin.Plugin
|
||||||
|
func (c *Plugin) Disable() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGotifyPluginInstance creates a plugin instance for a user context.
|
||||||
|
func NewGotifyPluginInstance(ctx plugin.UserContext) interface{} {
|
||||||
|
return &Plugin{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
panic("this is a broken plugin for testing purposes")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gotify/plugin-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetGotifyPluginInfo returns gotify plugin info
|
||||||
|
func GetGotifyPluginInfo() plugin.Info {
|
||||||
|
return plugin.Info{
|
||||||
|
ModulePath: "github.com/gotify/server/plugin/testing/broken/noinstance",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
panic("this is a broken plugin for testing purposes")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
panic("this is a broken plugin for testing purposes")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
// GetGotifyPluginInfo returns gotify plugin info
|
||||||
|
func GetGotifyPluginInfo() string {
|
||||||
|
return "github.com/gotify/server/plugin/testing/broken/unknowninfo"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
panic("this is a broken plugin for testing purposes")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
package mock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/gotify/server/plugin/compat"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ModulePath is for convenient access of the module path of this mock plugin
|
||||||
|
const ModulePath = "github.com/gotify/server/plugin/testing/mock"
|
||||||
|
|
||||||
|
// Name is for convenient access of the module path of the name of this mock plugin
|
||||||
|
const Name = "mock plugin"
|
||||||
|
|
||||||
|
// Plugin is a mock plugin.
|
||||||
|
type Plugin struct {
|
||||||
|
Instances []PluginInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginInfo implements loader.PluginCompat
|
||||||
|
func (c *Plugin) PluginInfo() compat.Info {
|
||||||
|
return compat.Info{
|
||||||
|
ModulePath: ModulePath,
|
||||||
|
Name: Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPluginInstance implements loader.PluginCompat
|
||||||
|
func (c *Plugin) NewPluginInstance(ctx compat.UserContext) compat.PluginInstance {
|
||||||
|
inst := PluginInstance{UserCtx: ctx, capabilities: compat.Capabilities{compat.Configurer, compat.Storager, compat.Messenger, compat.Displayer}}
|
||||||
|
c.Instances = append(c.Instances, inst)
|
||||||
|
return &inst
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIVersion implements loader.PluginCompat
|
||||||
|
func (c *Plugin) APIVersion() string {
|
||||||
|
return "v1"
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginInstance is a mock plugin instance
|
||||||
|
type PluginInstance struct {
|
||||||
|
UserCtx compat.UserContext
|
||||||
|
Enabled bool
|
||||||
|
DisplayString string
|
||||||
|
Config *PluginConfig
|
||||||
|
storageHandler compat.StorageHandler
|
||||||
|
messageHandler compat.MessageHandler
|
||||||
|
capabilities compat.Capabilities
|
||||||
|
BasePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginConfig is a mock plugin config struct
|
||||||
|
type PluginConfig struct {
|
||||||
|
TestKey string
|
||||||
|
IsNotValid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var disableFailUsers = make(map[uint]error)
|
||||||
|
var enableFailUsers = make(map[uint]error)
|
||||||
|
|
||||||
|
// ReturnErrorOnEnableForUser registers a uid which will throw an error on enabling.
|
||||||
|
func ReturnErrorOnEnableForUser(uid uint, err error) {
|
||||||
|
enableFailUsers[uid] = err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReturnErrorOnDisableForUser registers a uid which will throw an error on disabling.
|
||||||
|
func ReturnErrorOnDisableForUser(uid uint, err error) {
|
||||||
|
disableFailUsers[uid] = err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable implements compat.PluginInstance
|
||||||
|
func (c *PluginInstance) Enable() error {
|
||||||
|
if err, ok := enableFailUsers[c.UserCtx.ID]; ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Enabled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable implements compat.PluginInstance
|
||||||
|
func (c *PluginInstance) Disable() error {
|
||||||
|
if err, ok := disableFailUsers[c.UserCtx.ID]; ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Enabled = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMessageHandler implements compat.Messenger
|
||||||
|
func (c *PluginInstance) SetMessageHandler(h compat.MessageHandler) {
|
||||||
|
c.messageHandler = h
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStorageHandler implements compat.Storager
|
||||||
|
func (c *PluginInstance) SetStorageHandler(handler compat.StorageHandler) {
|
||||||
|
c.storageHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStorage sets current storage
|
||||||
|
func (c *PluginInstance) SetStorage(b []byte) error {
|
||||||
|
return c.storageHandler.Save(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStorage sets current storage
|
||||||
|
func (c *PluginInstance) GetStorage() ([]byte, error) {
|
||||||
|
return c.storageHandler.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterWebhook implements compat.Webhooker
|
||||||
|
func (c *PluginInstance) RegisterWebhook(basePath string, mux *gin.RouterGroup) {
|
||||||
|
c.BasePath = basePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCapability changes the capability of this plugin
|
||||||
|
func (c *PluginInstance) SetCapability(p compat.Capability, enable bool) {
|
||||||
|
if enable {
|
||||||
|
for _, cap := range c.capabilities {
|
||||||
|
if cap == p {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.capabilities = append(c.capabilities, p)
|
||||||
|
} else {
|
||||||
|
newCap := make(compat.Capabilities, 0)
|
||||||
|
for _, cap := range c.capabilities {
|
||||||
|
if cap == p {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newCap = append(newCap, cap)
|
||||||
|
}
|
||||||
|
c.capabilities = newCap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supports implements compat.PluginInstance
|
||||||
|
func (c *PluginInstance) Supports() compat.Capabilities {
|
||||||
|
return c.capabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig implements compat.Configuror
|
||||||
|
func (c *PluginInstance) DefaultConfig() interface{} {
|
||||||
|
return &PluginConfig{
|
||||||
|
TestKey: "default",
|
||||||
|
IsNotValid: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAndSetConfig implements compat.Configuror
|
||||||
|
func (c *PluginInstance) ValidateAndSetConfig(config interface{}) error {
|
||||||
|
if (config.(*PluginConfig)).IsNotValid {
|
||||||
|
return errors.New("conf is not valid")
|
||||||
|
}
|
||||||
|
c.Config = config.(*PluginConfig)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDisplay implements compat.Displayer
|
||||||
|
func (c *PluginInstance) GetDisplay(url *url.URL) string {
|
||||||
|
return c.DisplayString
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerMessage triggers a test message
|
||||||
|
func (c *PluginInstance) TriggerMessage() {
|
||||||
|
c.messageHandler.SendMessage(compat.Message{
|
||||||
|
Title: "test message",
|
||||||
|
Message: "test",
|
||||||
|
Priority: 2,
|
||||||
|
Extras: map[string]interface{}{
|
||||||
|
"test::string": "test",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -14,11 +14,17 @@ import (
|
||||||
"github.com/gotify/server/error"
|
"github.com/gotify/server/error"
|
||||||
"github.com/gotify/server/mode"
|
"github.com/gotify/server/mode"
|
||||||
"github.com/gotify/server/model"
|
"github.com/gotify/server/model"
|
||||||
|
"github.com/gotify/server/plugin"
|
||||||
"github.com/gotify/server/ui"
|
"github.com/gotify/server/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create creates the gin engine with all routes.
|
// Create creates the gin engine with all routes.
|
||||||
func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Configuration) (*gin.Engine, func()) {
|
func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Configuration) (*gin.Engine, func()) {
|
||||||
|
g := gin.New()
|
||||||
|
|
||||||
|
g.Use(gin.Logger(), gin.Recovery(), error.Handler(), location.Default())
|
||||||
|
g.NoRoute(error.NotFound())
|
||||||
|
|
||||||
streamHandler := stream.New(200*time.Second, 15*time.Second, conf.Server.Stream.AllowedOrigins)
|
streamHandler := stream.New(200*time.Second, 15*time.Second, conf.Server.Stream.AllowedOrigins)
|
||||||
authentication := auth.Auth{DB: db}
|
authentication := auth.Auth{DB: db}
|
||||||
messageHandler := api.MessageAPI{Notifier: streamHandler, DB: db}
|
messageHandler := api.MessageAPI{Notifier: streamHandler, DB: db}
|
||||||
|
|
@ -31,12 +37,22 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
|
||||||
DB: db,
|
DB: db,
|
||||||
ImageDir: conf.UploadedImagesDir,
|
ImageDir: conf.UploadedImagesDir,
|
||||||
}
|
}
|
||||||
userHandler := api.UserAPI{DB: db, PasswordStrength: conf.PassStrength, NotifyDeleted: streamHandler.NotifyDeletedUser}
|
userChangeNotifier := new(api.UserChangeNotifier)
|
||||||
|
userHandler := api.UserAPI{DB: db, PasswordStrength: conf.PassStrength, UserChangeNotifier: userChangeNotifier}
|
||||||
|
|
||||||
g := gin.New()
|
pluginManager, err := plugin.NewManager(db, conf.PluginsDir, g.Group("/plugin/:id/custom/"), streamHandler)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
pluginHandler := api.PluginAPI{
|
||||||
|
Manager: pluginManager,
|
||||||
|
Notifier: streamHandler,
|
||||||
|
DB: db,
|
||||||
|
}
|
||||||
|
|
||||||
g.Use(gin.Logger(), gin.Recovery(), error.Handler(), location.Default())
|
userChangeNotifier.OnUserDeleted(streamHandler.NotifyDeletedUser)
|
||||||
g.NoRoute(error.NotFound())
|
userChangeNotifier.OnUserDeleted(pluginManager.RemoveUser)
|
||||||
|
userChangeNotifier.OnUserAdded(pluginManager.InitializeForUserID)
|
||||||
|
|
||||||
ui.Register(g)
|
ui.Register(g)
|
||||||
|
|
||||||
|
|
@ -58,6 +74,18 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
{
|
||||||
|
g.GET("/plugin", authentication.RequireClient(), pluginHandler.GetPlugins)
|
||||||
|
pluginRoute := g.Group("/plugin/", authentication.RequireClient())
|
||||||
|
{
|
||||||
|
pluginRoute.GET("/:id/config", pluginHandler.GetConfig)
|
||||||
|
pluginRoute.POST("/:id/config", pluginHandler.UpdateConfig)
|
||||||
|
pluginRoute.GET("/:id/display", pluginHandler.GetDisplay)
|
||||||
|
pluginRoute.POST("/:id/enable", pluginHandler.EnablePlugin)
|
||||||
|
pluginRoute.POST("/:id/disable", pluginHandler.DisablePlugin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
g.OPTIONS("/*any")
|
g.OPTIONS("/*any")
|
||||||
|
|
||||||
// swagger:operation GET /version version getVersion
|
// swagger:operation GET /version version getVersion
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,17 @@ package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/json"
|
|
||||||
"github.com/gotify/server/config"
|
"github.com/gotify/server/config"
|
||||||
"github.com/gotify/server/mode"
|
"github.com/gotify/server/mode"
|
||||||
"github.com/gotify/server/model"
|
"github.com/gotify/server/model"
|
||||||
"github.com/gotify/server/test"
|
"github.com/gotify/server/test/testdb"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
@ -28,7 +28,7 @@ func TestIntegrationSuite(t *testing.T) {
|
||||||
|
|
||||||
type IntegrationSuite struct {
|
type IntegrationSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
db *test.Database
|
db *testdb.Database
|
||||||
server *httptest.Server
|
server *httptest.Server
|
||||||
closable func()
|
closable func()
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ type IntegrationSuite struct {
|
||||||
func (s *IntegrationSuite) BeforeTest(string, string) {
|
func (s *IntegrationSuite) BeforeTest(string, string) {
|
||||||
mode.Set(mode.TestDev)
|
mode.Set(mode.TestDev)
|
||||||
var err error
|
var err error
|
||||||
s.db = test.NewDBWithDefaultUser(s.T())
|
s.db = testdb.NewDBWithDefaultUser(s.T())
|
||||||
assert.Nil(s.T(), err)
|
assert.Nil(s.T(), err)
|
||||||
g, closable := Create(s.db.GormDatabase,
|
g, closable := Create(s.db.GormDatabase,
|
||||||
&model.VersionInfo{Version: "1.0.0", BuildDate: "2018-02-20-17:30:47", Commit: "asdasds"},
|
&model.VersionInfo{Version: "1.0.0", BuildDate: "2018-02-20-17:30:47", Commit: "asdasds"},
|
||||||
|
|
@ -78,7 +78,7 @@ func (s *IntegrationSuite) TestHeaderInProd() {
|
||||||
|
|
||||||
func TestHeadersFromConfiguration(t *testing.T) {
|
func TestHeadersFromConfiguration(t *testing.T) {
|
||||||
mode.Set(mode.Prod)
|
mode.Set(mode.Prod)
|
||||||
db := test.NewDBWithDefaultUser(t)
|
db := testdb.NewDBWithDefaultUser(t)
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
config := config.Configuration{PassStrength: 5}
|
config := config.Configuration{PassStrength: 5}
|
||||||
|
|
@ -148,6 +148,17 @@ func (s *IntegrationSuite) TestSendMessage() {
|
||||||
assert.Equal(s.T(), token.ID, msg.ApplicationID)
|
assert.Equal(s.T(), token.ID, msg.ApplicationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationSuite) TestPluginLoadFail_expectPanic() {
|
||||||
|
db := testdb.NewDBWithDefaultUser(s.T())
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
assert.Panics(s.T(), func() {
|
||||||
|
Create(db.GormDatabase, new(model.VersionInfo), &config.Configuration{
|
||||||
|
PluginsDir: "<THIS_PATH_IS_MALFORMED>",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *IntegrationSuite) TestAuthentication() {
|
func (s *IntegrationSuite) TestAuthentication() {
|
||||||
req := s.newRequest("GET", "current/user", "")
|
req := s.newRequest("GET", "current/user", "")
|
||||||
req.SetBasicAuth("admin", "pw")
|
req.SetBasicAuth("admin", "pw")
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
||||||
|
|
@ -25,3 +27,14 @@ func JSONEquals(t assert.TestingT, obj interface{}, expected string) {
|
||||||
|
|
||||||
assert.JSONEq(t, expected, objJSON)
|
assert.JSONEq(t, expected, objJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type unreadableReader struct{}
|
||||||
|
|
||||||
|
func (c unreadableReader) Read([]byte) (int, error) {
|
||||||
|
return 0, errors.New("this reader cannot be read")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnreadableReader returns an unreadadbe reader, used to mock IO issues.
|
||||||
|
func UnreadableReader() io.Reader {
|
||||||
|
return unreadableReader{}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package test_test
|
package test_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -40,3 +41,8 @@ func Test_BodyEquals_failing(t *testing.T) {
|
||||||
test.BodyEquals(fakeTesting, &obj{ID: 2, Test: "asd"}, recorder)
|
test.BodyEquals(fakeTesting, &obj{ID: 2, Test: "asd"}, recorder)
|
||||||
assert.True(t, fakeTesting.hasErrors)
|
assert.True(t, fakeTesting.hasErrors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_UnreaableReader(t *testing.T) {
|
||||||
|
_, err := ioutil.ReadAll(test.UnreadableReader())
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,5 @@ import (
|
||||||
// WithUser fake an authentication for testing.
|
// WithUser fake an authentication for testing.
|
||||||
func WithUser(ctx *gin.Context, userID uint) {
|
func WithUser(ctx *gin.Context, userID uint) {
|
||||||
ctx.Set("user", &model.User{ID: userID})
|
ctx.Set("user", &model.User{ID: userID})
|
||||||
|
ctx.Set("userid", userID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetProjectDir returns the currect apsolute path of this project
|
||||||
|
func GetProjectDir() string {
|
||||||
|
_, f, _, _ := runtime.Caller(0)
|
||||||
|
projectDir, _ := filepath.Abs(path.Join(filepath.Dir(f), "../"))
|
||||||
|
return projectDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithWd executes a function with the specified working directory
|
||||||
|
func WithWd(chDir string, f func(origWd string)) {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chdir(chDir); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer os.Chdir(wd)
|
||||||
|
f(wd)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProjectPath(t *testing.T) {
|
||||||
|
_, err := os.Stat(path.Join(GetProjectDir(), "./README.md"))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithWd(t *testing.T) {
|
||||||
|
wd1, _ := os.Getwd()
|
||||||
|
tmpDir := NewTmpDir("gotify_withwd")
|
||||||
|
defer tmpDir.Clean()
|
||||||
|
var wd2 string
|
||||||
|
WithWd(tmpDir.Path(), func(origWd string) {
|
||||||
|
assert.Equal(t, wd1, origWd)
|
||||||
|
wd2, _ = os.Getwd()
|
||||||
|
})
|
||||||
|
wd3, _ := os.Getwd()
|
||||||
|
assert.Equal(t, wd1, wd3)
|
||||||
|
assert.Equal(t, tmpDir.Path(), wd2)
|
||||||
|
assert.Nil(t, os.RemoveAll(tmpDir.Path()))
|
||||||
|
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
WithWd("non_exist", func(string) {})
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Nil(t, os.Mkdir(tmpDir.Path(), 0644))
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
WithWd(tmpDir.Path(), func(string) {})
|
||||||
|
})
|
||||||
|
assert.Nil(t, os.Remove(tmpDir.Path()))
|
||||||
|
|
||||||
|
assert.Nil(t, os.Mkdir(tmpDir.Path(), 0755))
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
WithWd(tmpDir.Path(), func(string) {
|
||||||
|
assert.Nil(t, os.RemoveAll(tmpDir.Path()))
|
||||||
|
WithWd(".", func(string) {})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package test
|
package testdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -64,31 +64,76 @@ func (d *Database) NewUserWithName(id uint, name string) *model.User {
|
||||||
|
|
||||||
// App creates an application and returns a message builder.
|
// App creates an application and returns a message builder.
|
||||||
func (ab *AppClientBuilder) App(id uint) *MessageBuilder {
|
func (ab *AppClientBuilder) App(id uint) *MessageBuilder {
|
||||||
return ab.AppWithToken(id, "app"+fmt.Sprint(id))
|
return ab.app(id, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InternalApp creates an internal application and returns a message builder.
|
||||||
|
func (ab *AppClientBuilder) InternalApp(id uint) *MessageBuilder {
|
||||||
|
return ab.app(id, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ab *AppClientBuilder) app(id uint, internal bool) *MessageBuilder {
|
||||||
|
return ab.appWithToken(id, "app"+fmt.Sprint(id), internal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppWithToken creates an application with a token and returns a message builder.
|
// AppWithToken creates an application with a token and returns a message builder.
|
||||||
func (ab *AppClientBuilder) AppWithToken(id uint, token string) *MessageBuilder {
|
func (ab *AppClientBuilder) AppWithToken(id uint, token string) *MessageBuilder {
|
||||||
ab.NewAppWithToken(id, token)
|
return ab.appWithToken(id, token, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InternalAppWithToken creates an internal application with a token and returns a message builder.
|
||||||
|
func (ab *AppClientBuilder) InternalAppWithToken(id uint, token string) *MessageBuilder {
|
||||||
|
return ab.appWithToken(id, token, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ab *AppClientBuilder) appWithToken(id uint, token string, internal bool) *MessageBuilder {
|
||||||
|
ab.newAppWithToken(id, token, internal)
|
||||||
return &MessageBuilder{db: ab.db, appID: id}
|
return &MessageBuilder{db: ab.db, appID: id}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAppWithToken creates an application with a token and returns the app.
|
// NewAppWithToken creates an application with a token and returns the app.
|
||||||
func (ab *AppClientBuilder) NewAppWithToken(id uint, token string) *model.Application {
|
func (ab *AppClientBuilder) NewAppWithToken(id uint, token string) *model.Application {
|
||||||
application := &model.Application{ID: id, UserID: ab.userID, Token: token}
|
return ab.newAppWithToken(id, token, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInternalAppWithToken creates an internal application with a token and returns the app.
|
||||||
|
func (ab *AppClientBuilder) NewInternalAppWithToken(id uint, token string) *model.Application {
|
||||||
|
return ab.newAppWithToken(id, token, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ab *AppClientBuilder) newAppWithToken(id uint, token string, internal bool) *model.Application {
|
||||||
|
application := &model.Application{ID: id, UserID: ab.userID, Token: token, Internal: internal}
|
||||||
ab.db.CreateApplication(application)
|
ab.db.CreateApplication(application)
|
||||||
return application
|
return application
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppWithTokenAndName creates an application with a token and name and returns a message builder.
|
// AppWithTokenAndName creates an application with a token and name and returns a message builder.
|
||||||
func (ab *AppClientBuilder) AppWithTokenAndName(id uint, token, name string) *MessageBuilder {
|
func (ab *AppClientBuilder) AppWithTokenAndName(id uint, token, name string) *MessageBuilder {
|
||||||
ab.NewAppWithTokenAndName(id, token, name)
|
return ab.appWithTokenAndName(id, token, name, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InternalAppWithTokenAndName creates an internal application with a token and name and returns a message builder.
|
||||||
|
func (ab *AppClientBuilder) InternalAppWithTokenAndName(id uint, token, name string) *MessageBuilder {
|
||||||
|
return ab.appWithTokenAndName(id, token, name, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ab *AppClientBuilder) appWithTokenAndName(id uint, token, name string, internal bool) *MessageBuilder {
|
||||||
|
ab.newAppWithTokenAndName(id, token, name, internal)
|
||||||
return &MessageBuilder{db: ab.db, appID: id}
|
return &MessageBuilder{db: ab.db, appID: id}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAppWithTokenAndName creates an application with a token and name and returns the app.
|
// NewAppWithTokenAndName creates an application with a token and name and returns the app.
|
||||||
func (ab *AppClientBuilder) NewAppWithTokenAndName(id uint, token, name string) *model.Application {
|
func (ab *AppClientBuilder) NewAppWithTokenAndName(id uint, token, name string) *model.Application {
|
||||||
application := &model.Application{ID: id, UserID: ab.userID, Token: token, Name: name}
|
return ab.newAppWithTokenAndName(id, token, name, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInternalAppWithTokenAndName creates an internal application with a token and name and returns the app.
|
||||||
|
func (ab *AppClientBuilder) NewInternalAppWithTokenAndName(id uint, token, name string) *model.Application {
|
||||||
|
return ab.newAppWithTokenAndName(id, token, name, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ab *AppClientBuilder) newAppWithTokenAndName(id uint, token, name string, internal bool) *model.Application {
|
||||||
|
application := &model.Application{ID: id, UserID: ab.userID, Token: token, Name: name, Internal: internal}
|
||||||
ab.db.CreateApplication(application)
|
ab.db.CreateApplication(application)
|
||||||
return application
|
return application
|
||||||
}
|
}
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
package test_test
|
package testdb_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotify/server/mode"
|
"github.com/gotify/server/mode"
|
||||||
"github.com/gotify/server/model"
|
"github.com/gotify/server/model"
|
||||||
"github.com/gotify/server/test"
|
"github.com/gotify/server/test/testdb"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_WithDefault(t *testing.T) {
|
func Test_WithDefault(t *testing.T) {
|
||||||
db := test.NewDBWithDefaultUser(t)
|
db := testdb.NewDBWithDefaultUser(t)
|
||||||
assert.NotNil(t, db.GetUserByName("admin"))
|
assert.NotNil(t, db.GetUserByName("admin"))
|
||||||
db.Close()
|
db.Close()
|
||||||
}
|
}
|
||||||
|
|
@ -22,12 +22,12 @@ func TestDatabaseSuite(t *testing.T) {
|
||||||
|
|
||||||
type DatabaseSuite struct {
|
type DatabaseSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
db *test.Database
|
db *testdb.Database
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DatabaseSuite) BeforeTest(suiteName, testName string) {
|
func (s *DatabaseSuite) BeforeTest(suiteName, testName string) {
|
||||||
mode.Set(mode.TestDev)
|
mode.Set(mode.TestDev)
|
||||||
s.db = test.NewDB(s.T())
|
s.db = testdb.NewDB(s.T())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DatabaseSuite) AfterTest(suiteName, testName string) {
|
func (s *DatabaseSuite) AfterTest(suiteName, testName string) {
|
||||||
|
|
@ -88,32 +88,46 @@ func (s *DatabaseSuite) Test_Apps() {
|
||||||
userBuilder := s.db.User(1)
|
userBuilder := s.db.User(1)
|
||||||
userBuilder.App(1)
|
userBuilder.App(1)
|
||||||
newAppActual := userBuilder.NewAppWithToken(2, "asdf")
|
newAppActual := userBuilder.NewAppWithToken(2, "asdf")
|
||||||
|
newInternalAppActual := userBuilder.NewInternalAppWithToken(3, "qwer")
|
||||||
|
|
||||||
s.db.User(2).App(5)
|
s.db.User(2).InternalApp(5)
|
||||||
|
|
||||||
newAppExpected := &model.Application{ID: 2, Token: "asdf", UserID: 1}
|
newAppExpected := &model.Application{ID: 2, Token: "asdf", UserID: 1}
|
||||||
|
newInternalAppExpected := &model.Application{ID: 3, Token: "qwer", UserID: 1, Internal: true}
|
||||||
|
|
||||||
assert.Equal(s.T(), newAppExpected, newAppActual)
|
assert.Equal(s.T(), newAppExpected, newAppActual)
|
||||||
|
assert.Equal(s.T(), newInternalAppExpected, newInternalAppActual)
|
||||||
|
|
||||||
userOneExpected := []*model.Application{{ID: 1, Token: "app1", UserID: 1}, {ID: 2, Token: "asdf", UserID: 1}}
|
userOneExpected := []*model.Application{{ID: 1, Token: "app1", UserID: 1}, {ID: 2, Token: "asdf", UserID: 1}, {ID: 3, Token: "qwer", UserID: 1, Internal: true}}
|
||||||
assert.Equal(s.T(), userOneExpected, s.db.GetApplicationsByUser(1))
|
assert.Equal(s.T(), userOneExpected, s.db.GetApplicationsByUser(1))
|
||||||
userTwoExpected := []*model.Application{{ID: 5, Token: "app5", UserID: 2}}
|
userTwoExpected := []*model.Application{{ID: 5, Token: "app5", UserID: 2, Internal: true}}
|
||||||
assert.Equal(s.T(), userTwoExpected, s.db.GetApplicationsByUser(2))
|
assert.Equal(s.T(), userTwoExpected, s.db.GetApplicationsByUser(2))
|
||||||
|
|
||||||
newAppWithName := userBuilder.NewAppWithTokenAndName(7, "test-token", "app name")
|
newAppWithName := userBuilder.NewAppWithTokenAndName(7, "test-token", "app name")
|
||||||
newAppWithNameExpected := &model.Application{ID: 7, Token: "test-token", UserID: 1, Name: "app name"}
|
newAppWithNameExpected := &model.Application{ID: 7, Token: "test-token", UserID: 1, Name: "app name"}
|
||||||
assert.Equal(s.T(), newAppWithNameExpected, newAppWithName)
|
assert.Equal(s.T(), newAppWithNameExpected, newAppWithName)
|
||||||
|
|
||||||
userBuilder.AppWithTokenAndName(8, "test-token-2", "app name")
|
newInternalAppWithName := userBuilder.NewInternalAppWithTokenAndName(8, "test-tokeni", "app name")
|
||||||
|
newInternalAppWithNameExpected := &model.Application{ID: 8, Token: "test-tokeni", UserID: 1, Name: "app name", Internal: true}
|
||||||
|
assert.Equal(s.T(), newInternalAppWithNameExpected, newInternalAppWithName)
|
||||||
|
|
||||||
|
userBuilder.AppWithTokenAndName(9, "test-token-2", "app name")
|
||||||
|
userBuilder.InternalAppWithTokenAndName(10, "test-tokeni-2", "app name")
|
||||||
|
userBuilder.AppWithToken(11, "test-token-3")
|
||||||
|
userBuilder.InternalAppWithToken(12, "test-tokeni-3")
|
||||||
|
|
||||||
s.db.AssertAppExist(1)
|
s.db.AssertAppExist(1)
|
||||||
s.db.AssertAppExist(2)
|
s.db.AssertAppExist(2)
|
||||||
s.db.AssertAppNotExist(3)
|
s.db.AssertAppExist(3)
|
||||||
s.db.AssertAppNotExist(4)
|
s.db.AssertAppNotExist(4)
|
||||||
s.db.AssertAppExist(5)
|
s.db.AssertAppExist(5)
|
||||||
s.db.AssertAppNotExist(6)
|
s.db.AssertAppNotExist(6)
|
||||||
s.db.AssertAppExist(7)
|
s.db.AssertAppExist(7)
|
||||||
s.db.AssertAppExist(8)
|
s.db.AssertAppExist(8)
|
||||||
|
s.db.AssertAppExist(9)
|
||||||
|
s.db.AssertAppExist(10)
|
||||||
|
s.db.AssertAppExist(11)
|
||||||
|
s.db.AssertAppExist(12)
|
||||||
|
|
||||||
s.db.DeleteApplicationByID(2)
|
s.db.DeleteApplicationByID(2)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TmpDir is a handler to temporary directory
|
||||||
|
type TmpDir struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path returns the path to the temporary directory joined by the elements provided
|
||||||
|
func (c TmpDir) Path(elem ...string) string {
|
||||||
|
return path.Join(append([]string{c.path}, elem...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean removes the TmpDir
|
||||||
|
func (c TmpDir) Clean() error {
|
||||||
|
return os.RemoveAll(c.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTmpDir returns a new handle to a tmp dir
|
||||||
|
func NewTmpDir(prefix string) TmpDir {
|
||||||
|
dir, _ := ioutil.TempDir("", prefix)
|
||||||
|
return TmpDir{dir}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTmpDir(t *testing.T) {
|
||||||
|
dir := NewTmpDir("test_prefix")
|
||||||
|
assert.NotEmpty(t, dir)
|
||||||
|
|
||||||
|
assert.Contains(t, dir.Path(), "test_prefix")
|
||||||
|
testFilePath := dir.Path("testfile.txt")
|
||||||
|
assert.Contains(t, testFilePath, "test_prefix")
|
||||||
|
assert.Contains(t, testFilePath, "testfile.txt")
|
||||||
|
assert.True(t, strings.HasPrefix(testFilePath, dir.Path()))
|
||||||
|
}
|
||||||
|
|
@ -174,12 +174,27 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/codemirror": {
|
||||||
|
"version": "0.0.71",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.71.tgz",
|
||||||
|
"integrity": "sha512-b2oEEnno1LIGKMR7uBEsr40al1UijF1HEpRn0+Yf1xOLl24iQgB7DBpZVMM7y54G5wCNoclDrRO65E6KHPNO2w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/tern": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/detect-browser": {
|
"@types/detect-browser": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/detect-browser/-/detect-browser-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/detect-browser/-/detect-browser-2.0.1.tgz",
|
||||||
"integrity": "sha512-n9jH0zq0DGOlu/B9tSpK+DKwaW9uozF96hy3zYibvxVqQDX5KOJJn6qns/G+k3UwfEQVYZuV3Rz+Z2fXMQ0ang==",
|
"integrity": "sha512-n9jH0zq0DGOlu/B9tSpK+DKwaW9uozF96hy3zYibvxVqQDX5KOJJn6qns/G+k3UwfEQVYZuV3Rz+Z2fXMQ0ang==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/estree": {
|
||||||
|
"version": "0.0.39",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
|
||||||
|
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/events": {
|
"@types/events": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz",
|
||||||
|
|
@ -333,6 +348,15 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/tern": {
|
||||||
|
"version": "0.22.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.22.1.tgz",
|
||||||
|
"integrity": "sha512-CRzPRkg8hYLwunsj61r+rqPJQbiCIEQqlMMY/0k7krgIsoSaFgGg1ZH2f9qaR1YpenaMl6PnlTtUkCbNH/uo+A==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/estree": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"abab": {
|
"abab": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz",
|
||||||
|
|
@ -1717,6 +1741,11 @@
|
||||||
"integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
|
"integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"bail": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-1X8CnjFVQ+a+KW36uBNMTU5s8+v5FzeqrP7hTG5aTb4aPreSbZJlhwPon9VKMuEVgV++JM+SQrALY3kr7eswdg=="
|
||||||
|
},
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "0.4.2",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
|
||||||
|
|
@ -2319,6 +2348,21 @@
|
||||||
"resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz",
|
||||||
"integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU="
|
"integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU="
|
||||||
},
|
},
|
||||||
|
"character-entities": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-sMoHX6/nBiy3KKfC78dnEalnpn0Az0oSNvqUWYTtYrhRI5iUIYsROU48G+E+kMFQzqXaJ8kHJZ85n7y6/PHgwQ=="
|
||||||
|
},
|
||||||
|
"character-entities-legacy": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-9NB2VbXtXYWdXzqrvAHykE/f0QJxzaKIpZ5QzNZrrgQ7Iyxr2vnfS8fCBNVW9nUEZE0lo57nxKRqnzY/dKrwlA=="
|
||||||
|
},
|
||||||
|
"character-reference-invalid": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7I/xceXfKyUJmSAn/jw8ve/9DyOP7XxufNYLI9Px7CmsKgEUaZLUTax6nZxGQtaoiZCjpu6cHPj20xC/vqRReQ=="
|
||||||
|
},
|
||||||
"chardet": {
|
"chardet": {
|
||||||
"version": "0.4.2",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
|
||||||
|
|
@ -2477,6 +2521,16 @@
|
||||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"codemirror": {
|
||||||
|
"version": "5.43.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.43.0.tgz",
|
||||||
|
"integrity": "sha512-mljwQWUaWIf85I7QwTBryF2ASaIvmYAL4s5UCanCJFfKeXOKhrqdHWdHiZWAMNT+hjLTCnVx2S/SYTORIgxsgA=="
|
||||||
|
},
|
||||||
|
"collapse-white-space": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-YfQ1tAUZm561vpYD+5eyWN8+UsceQbSrqqlc/6zDY2gtAE+uZLSdkkovhnGpmCThsvKBFakq4EdY/FF93E8XIw=="
|
||||||
|
},
|
||||||
"collection-visit": {
|
"collection-visit": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
|
||||||
|
|
@ -3421,7 +3475,6 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
|
||||||
"integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
|
"integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"domelementtype": "~1.1.1",
|
"domelementtype": "~1.1.1",
|
||||||
"entities": "~1.1.1"
|
"entities": "~1.1.1"
|
||||||
|
|
@ -3430,8 +3483,7 @@
|
||||||
"domelementtype": {
|
"domelementtype": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
|
"resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
|
||||||
"integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=",
|
"integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs="
|
||||||
"dev": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -3453,8 +3505,7 @@
|
||||||
"domelementtype": {
|
"domelementtype": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz",
|
||||||
"integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=",
|
"integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"domexception": {
|
"domexception": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
|
@ -3478,7 +3529,6 @@
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
|
||||||
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
|
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"dom-serializer": "0",
|
"dom-serializer": "0",
|
||||||
"domelementtype": "1"
|
"domelementtype": "1"
|
||||||
|
|
@ -3611,8 +3661,7 @@
|
||||||
"entities": {
|
"entities": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
|
||||||
"integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=",
|
"integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"enzyme-adapter-react-16": {
|
"enzyme-adapter-react-16": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
|
|
@ -3773,8 +3822,7 @@
|
||||||
"escape-string-regexp": {
|
"escape-string-regexp": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
|
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"escodegen": {
|
"escodegen": {
|
||||||
"version": "1.11.0",
|
"version": "1.11.0",
|
||||||
|
|
@ -4089,8 +4137,7 @@
|
||||||
"extend": {
|
"extend": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"extend-shallow": {
|
"extend-shallow": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
|
|
@ -4550,7 +4597,8 @@
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
|
@ -4571,12 +4619,14 @@
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
|
|
@ -4591,17 +4641,20 @@
|
||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
|
@ -4718,7 +4771,8 @@
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
|
|
@ -4730,6 +4784,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -4744,6 +4799,7 @@
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
|
|
@ -4751,12 +4807,14 @@
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
|
|
@ -4775,6 +4833,7 @@
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
|
|
@ -4855,7 +4914,8 @@
|
||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
|
|
@ -4867,6 +4927,7 @@
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
|
|
@ -4952,7 +5013,8 @@
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
|
|
@ -4988,6 +5050,7 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
|
|
@ -5007,6 +5070,7 @@
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^2.0.0"
|
"ansi-regex": "^2.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -5050,12 +5114,14 @@
|
||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -5526,6 +5592,51 @@
|
||||||
"uglify-js": "3.4.x"
|
"uglify-js": "3.4.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"html-to-react": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-/tWDdb/8Koi/QEP5YUY1653PcDpBnnMblXRhotnTuhFDjI1Fc6Wzox5d4sw73Xk5rM2OdM5np4AYjT/US/Wj7Q==",
|
||||||
|
"requires": {
|
||||||
|
"domhandler": "^2.4.2",
|
||||||
|
"escape-string-regexp": "^1.0.5",
|
||||||
|
"htmlparser2": "^3.10.0",
|
||||||
|
"lodash.camelcase": "^4.3.0",
|
||||||
|
"ramda": "^0.26"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"domhandler": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"htmlparser2": {
|
||||||
|
"version": "3.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.0.tgz",
|
||||||
|
"integrity": "sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^1.3.0",
|
||||||
|
"domhandler": "^2.3.0",
|
||||||
|
"domutils": "^1.5.1",
|
||||||
|
"entities": "^1.1.1",
|
||||||
|
"inherits": "^2.0.1",
|
||||||
|
"readable-stream": "^3.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"readable-stream": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==",
|
||||||
|
"requires": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"html-webpack-plugin": {
|
"html-webpack-plugin": {
|
||||||
"version": "2.29.0",
|
"version": "2.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-2.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-2.29.0.tgz",
|
||||||
|
|
@ -5918,8 +6029,7 @@
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
|
|
@ -6044,6 +6154,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"is-alphabetical": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-V0xN4BYezDHcBSKb1QHUFMlR4as/XEuCZBzMJUU4n7+Cbt33SmUnSol+pnXFvLxSHNq2CemUXNdaXV6Flg7+xg=="
|
||||||
|
},
|
||||||
|
"is-alphanumerical": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-pyfU/0kHdISIgslFfZN9nfY1Gk3MquQgUm1mJTjdkEPpkAKNWuBTSqFwewOpR7N351VkErCiyV71zX7mlQQqsg==",
|
||||||
|
"requires": {
|
||||||
|
"is-alphabetical": "^1.0.0",
|
||||||
|
"is-decimal": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"is-arrayish": {
|
"is-arrayish": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||||
|
|
@ -6112,6 +6236,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
|
||||||
"integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY="
|
"integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY="
|
||||||
},
|
},
|
||||||
|
"is-decimal": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-TRzl7mOCchnhchN+f3ICUCzYvL9ul7R+TYOsZ8xia++knyZAJfv/uA1FvQXsAnYIl1T3B2X5E/J7Wb1QXiIBXg=="
|
||||||
|
},
|
||||||
"is-descriptor": {
|
"is-descriptor": {
|
||||||
"version": "0.1.6",
|
"version": "0.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
|
||||||
|
|
@ -6199,6 +6328,11 @@
|
||||||
"is-extglob": "^1.0.0"
|
"is-extglob": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"is-hexadecimal": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-but/G3sapV3MNyqiDBLrOi4x8uCIw0RY3o/Vb5GT0sMFHrVV7731wFSVy41T5FO1og7G0gXLJh0MkgPRouko/A=="
|
||||||
|
},
|
||||||
"is-in-browser": {
|
"is-in-browser": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
|
||||||
|
|
@ -6273,8 +6407,7 @@
|
||||||
"is-plain-obj": {
|
"is-plain-obj": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
|
||||||
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
|
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"is-plain-object": {
|
"is-plain-object": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
|
|
@ -6359,12 +6492,22 @@
|
||||||
"integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
|
"integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"is-whitespace-character": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-SzM+T5GKUCtLhlHFKt2SDAX2RFzfS6joT91F2/WSi9LxgFdsnhfPK/UIA+JhRR2xuyLdrCys2PiFDrtn1fU5hQ=="
|
||||||
|
},
|
||||||
"is-windows": {
|
"is-windows": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
|
||||||
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
|
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"is-word-character": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-T3FlsX8rCHAH8e7RE7PfOPZVFQlcV3XRF9eOOBQ1uf70OxO7CjjSOjeImMPCADBdYWcStAbVbYvJ1m2D3tb+EA=="
|
||||||
|
},
|
||||||
"is-wsl": {
|
"is-wsl": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
|
||||||
|
|
@ -7731,8 +7874,7 @@
|
||||||
"lodash.camelcase": {
|
"lodash.camelcase": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||||
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=",
|
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"lodash.debounce": {
|
"lodash.debounce": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
|
|
@ -7916,6 +8058,11 @@
|
||||||
"object-visit": "^1.0.0"
|
"object-visit": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"markdown-escapes": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-lbRZ2mE3Q9RtLjxZBZ9+IMl68DKIXaVAhwvwn9pmjnPLS0h/6kyBMgNhqi1xFJ/2yv6cSyv0jbiZavZv93JkkA=="
|
||||||
|
},
|
||||||
"math-expression-evaluator": {
|
"math-expression-evaluator": {
|
||||||
"version": "1.2.17",
|
"version": "1.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz",
|
||||||
|
|
@ -7938,6 +8085,14 @@
|
||||||
"inherits": "^2.0.1"
|
"inherits": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mdast-add-list-metadata": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-fB/VP4MJ0LaRsog7hGPxgOrSL3gE/2uEdZyDuSEnKCv/8IkYHiDkIQSbChiJoHyxZZXZ9bzckyRk+vNxFzh8rA==",
|
||||||
|
"requires": {
|
||||||
|
"unist-util-visit-parents": "1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"media-typer": {
|
"media-typer": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
"resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
|
|
@ -8737,6 +8892,19 @@
|
||||||
"pbkdf2": "^3.0.3"
|
"pbkdf2": "^3.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"parse-entities": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-XXtDdOPLSB0sHecbEapQi6/58U/ODj/KWfIXmmMCJF/eRn8laX6LZbOyioMoETOOJoWRW8/qTSl5VQkUIfKM5g==",
|
||||||
|
"requires": {
|
||||||
|
"character-entities": "^1.0.0",
|
||||||
|
"character-entities-legacy": "^1.0.0",
|
||||||
|
"character-reference-invalid": "^1.0.0",
|
||||||
|
"is-alphanumerical": "^1.0.0",
|
||||||
|
"is-decimal": "^1.0.0",
|
||||||
|
"is-hexadecimal": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"parse-glob": {
|
"parse-glob": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
|
||||||
|
|
@ -8929,7 +9097,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": {
|
"async": {
|
||||||
"version": "1.5.2",
|
"version": "1.5.2",
|
||||||
"resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
||||||
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
|
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
|
|
@ -10492,6 +10660,11 @@
|
||||||
"performance-now": "^2.1.0"
|
"performance-now": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ramda": {
|
||||||
|
"version": "0.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz",
|
||||||
|
"integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ=="
|
||||||
|
},
|
||||||
"randomatic": {
|
"randomatic": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.0.tgz",
|
||||||
|
|
@ -10611,6 +10784,11 @@
|
||||||
"prop-types": "^15.6.0"
|
"prop-types": "^15.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-codemirror2": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-Cksbgbviuf2mJfMyrKmcu7ycK6zX/ukuQO8dvRZdFWqATf5joalhjFc6etnBdGCcPA2LbhIwz+OPnQxLN/j1Fw=="
|
||||||
|
},
|
||||||
"react-dev-utils": {
|
"react-dev-utils": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-5.0.2.tgz",
|
||||||
|
|
@ -10714,6 +10892,20 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||||
},
|
},
|
||||||
|
"react-markdown": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-E1d/q+OBk5eumId42oYqVrJRB/+whrZdk+YHqUBCCNeWxqeV+Qzt+yLTsft9+4HRDj89Od7eAbUPQBYq8ZwShQ==",
|
||||||
|
"requires": {
|
||||||
|
"html-to-react": "^1.3.4",
|
||||||
|
"mdast-add-list-metadata": "1.0.1",
|
||||||
|
"prop-types": "^15.6.1",
|
||||||
|
"remark-parse": "^5.0.0",
|
||||||
|
"unified": "^6.1.5",
|
||||||
|
"unist-util-visit": "^1.3.0",
|
||||||
|
"xtend": "^4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-reconciler": {
|
"react-reconciler": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.7.0.tgz",
|
||||||
|
|
@ -11090,7 +11282,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsesc": {
|
"jsesc": {
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
|
||||||
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
|
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
|
|
@ -11102,6 +11294,28 @@
|
||||||
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
|
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"remark-parse": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-b3iXszZLH1TLoyUzrATcTQUZrwNl1rE70rVdSruJFlDaJ9z5aMkhrG43Pp68OgfHndL/ADz6V69Zow8cTQu+JA==",
|
||||||
|
"requires": {
|
||||||
|
"collapse-white-space": "^1.0.2",
|
||||||
|
"is-alphabetical": "^1.0.0",
|
||||||
|
"is-decimal": "^1.0.0",
|
||||||
|
"is-whitespace-character": "^1.0.0",
|
||||||
|
"is-word-character": "^1.0.0",
|
||||||
|
"markdown-escapes": "^1.0.0",
|
||||||
|
"parse-entities": "^1.1.0",
|
||||||
|
"repeat-string": "^1.5.4",
|
||||||
|
"state-toggle": "^1.0.0",
|
||||||
|
"trim": "0.0.1",
|
||||||
|
"trim-trailing-lines": "^1.0.0",
|
||||||
|
"unherit": "^1.0.4",
|
||||||
|
"unist-util-remove-position": "^1.0.0",
|
||||||
|
"vfile-location": "^2.0.0",
|
||||||
|
"xtend": "^4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"remove-trailing-separator": {
|
"remove-trailing-separator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
|
||||||
|
|
@ -11138,8 +11352,7 @@
|
||||||
"repeat-string": {
|
"repeat-string": {
|
||||||
"version": "1.6.1",
|
"version": "1.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
|
||||||
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
|
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"repeating": {
|
"repeating": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
|
|
@ -11150,6 +11363,11 @@
|
||||||
"is-finite": "^1.0.0"
|
"is-finite": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"replace-ext": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs="
|
||||||
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"version": "2.88.0",
|
"version": "2.88.0",
|
||||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
|
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
|
||||||
|
|
@ -11359,8 +11577,7 @@
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"safe-regex": {
|
"safe-regex": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
|
|
@ -11956,6 +12173,11 @@
|
||||||
"integrity": "sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=",
|
"integrity": "sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"state-toggle": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-Qe8QntFrrpWTnHwvwj2FZTgv+PKIsp0B9VxLzLLbSpPXWOgRgc5LVj/aTiSfK1RqIeF9jeC1UeOH8Q8y60A7og=="
|
||||||
|
},
|
||||||
"static-extend": {
|
"static-extend": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
|
||||||
|
|
@ -12092,7 +12314,6 @@
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
|
|
@ -12448,6 +12669,11 @@
|
||||||
"integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==",
|
"integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"trim": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz",
|
||||||
|
"integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0="
|
||||||
|
},
|
||||||
"trim-newlines": {
|
"trim-newlines": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
|
||||||
|
|
@ -12460,6 +12686,16 @@
|
||||||
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
|
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"trim-trailing-lines": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-bWLv9BbWbbd7mlqqs2oQYnLD/U/ZqeJeJwbO0FG2zA1aTq+HTvxfHNKFa/HGCVyJpDiioUYaBhfiT6rgk+l4mg=="
|
||||||
|
},
|
||||||
|
"trough": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/trough/-/trough-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-fwkLWH+DimvA4YCy+/nvJd61nWQQ2liO/nF/RjkTpiOGi+zxZzVkhb1mvbHIIW4b/8nDsYI8uTmAlc0nNkRMOw=="
|
||||||
|
},
|
||||||
"ts-jest": {
|
"ts-jest": {
|
||||||
"version": "22.0.1",
|
"version": "22.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-22.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-22.0.1.tgz",
|
||||||
|
|
@ -12540,7 +12776,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json5": {
|
"json5": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|
@ -12549,7 +12785,7 @@
|
||||||
},
|
},
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
|
@ -12812,6 +13048,28 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"unherit": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-+XZuV691Cn4zHsK0vkKYwBEwB74T3IZIcxrgn2E4rKwTfFyI1zCh7X7grwh9Re08fdPlarIdyWgI8aVB3F5A5g==",
|
||||||
|
"requires": {
|
||||||
|
"inherits": "^2.0.1",
|
||||||
|
"xtend": "^4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unified": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==",
|
||||||
|
"requires": {
|
||||||
|
"bail": "^1.0.0",
|
||||||
|
"extend": "^3.0.0",
|
||||||
|
"is-plain-obj": "^1.1.0",
|
||||||
|
"trough": "^1.0.0",
|
||||||
|
"vfile": "^2.0.0",
|
||||||
|
"x-is-string": "^0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"union-value": {
|
"union-value": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
|
||||||
|
|
@ -12886,6 +13144,47 @@
|
||||||
"crypto-random-string": "^1.0.0"
|
"crypto-random-string": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"unist-util-is": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YkXBK/H9raAmG7KXck+UUpnKiNmUdB+aBGrknfQ4EreE1banuzrKABx3jP6Z5Z3fMSPMQQmeXBlKpCbMwBkxVw=="
|
||||||
|
},
|
||||||
|
"unist-util-remove-position": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-XxoNOBvq1WXRKXxgnSYbtCF76TJrRoe5++pD4cCBsssSiWSnPEktyFrFLE8LTk3JW5mt9hB0Sk5zn4x/JeWY7Q==",
|
||||||
|
"requires": {
|
||||||
|
"unist-util-visit": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unist-util-stringify-position": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ=="
|
||||||
|
},
|
||||||
|
"unist-util-visit": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-FiGu34ziNsZA3ZUteZxSFaczIjGmksfSgdKqBfOejrrfzyUy5b7YrlzT1Bcvi+djkYDituJDy2XB7tGTeBieKw==",
|
||||||
|
"requires": {
|
||||||
|
"unist-util-visit-parents": "^2.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"unist-util-visit-parents": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-6B0UTiMfdWql4cQ03gDTCSns+64Zkfo2OCbK31Ov0uMizEz+CJeAp0cgZVb5Fhmcd7Bct2iRNywejT0orpbqUA==",
|
||||||
|
"requires": {
|
||||||
|
"unist-util-is": "^2.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unist-util-visit-parents": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-yvo+MMLjEwdc3RhhPYSximset7rwjMrdt9E41Smmvg25UQIenzrN83cRnF1JMzoMi9zZOQeYXHSDf7p+IQkW3Q=="
|
||||||
|
},
|
||||||
"universalify": {
|
"universalify": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||||
|
|
@ -13074,8 +13373,7 @@
|
||||||
"util-deprecate": {
|
"util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
|
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"util.promisify": {
|
"util.promisify": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|
@ -13143,6 +13441,30 @@
|
||||||
"extsprintf": "^1.2.0"
|
"extsprintf": "^1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"vfile": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==",
|
||||||
|
"requires": {
|
||||||
|
"is-buffer": "^1.1.4",
|
||||||
|
"replace-ext": "1.0.0",
|
||||||
|
"unist-util-stringify-position": "^1.0.0",
|
||||||
|
"vfile-message": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vfile-location": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-KRL5uXQPoUKu+NGvQVL4XLORw45W62v4U4gxJ3vRlDfI9QsT4ZN1PNXn/zQpKUulqGDpYuT0XDfp5q9O87/y/w=="
|
||||||
|
},
|
||||||
|
"vfile-message": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==",
|
||||||
|
"requires": {
|
||||||
|
"unist-util-stringify-position": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"vm-browserify": {
|
"vm-browserify": {
|
||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
|
||||||
|
|
@ -13983,6 +14305,11 @@
|
||||||
"async-limiter": "~1.0.0"
|
"async-limiter": "~1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"x-is-string": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz",
|
||||||
|
"integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI="
|
||||||
|
},
|
||||||
"xdg-basedir": {
|
"xdg-basedir": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz",
|
||||||
|
|
@ -13998,8 +14325,7 @@
|
||||||
"xtend": {
|
"xtend": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
|
||||||
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
|
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"y18n": {
|
"y18n": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
"@material-ui/core": "^1.5.1",
|
"@material-ui/core": "^1.5.1",
|
||||||
"@material-ui/icons": "^2.0.3",
|
"@material-ui/icons": "^2.0.3",
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
|
"codemirror": "^5.43.0",
|
||||||
"detect-browser": "^3.0.0",
|
"detect-browser": "^3.0.0",
|
||||||
"mobx": "^5.1.1",
|
"mobx": "^5.1.1",
|
||||||
"mobx-react": "^5.2.8",
|
"mobx-react": "^5.2.8",
|
||||||
|
|
@ -13,8 +14,10 @@
|
||||||
"notifyjs": "^3.0.0",
|
"notifyjs": "^3.0.0",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.4.2",
|
"react": "^16.4.2",
|
||||||
|
"react-codemirror2": "^5.1.0",
|
||||||
"react-dom": "^16.4.2",
|
"react-dom": "^16.4.2",
|
||||||
"react-infinite": "^0.13.0",
|
"react-infinite": "^0.13.0",
|
||||||
|
"react-markdown": "^4.0.6",
|
||||||
"react-router": "^4.3.1",
|
"react-router": "^4.3.1",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
"react-timeago": "^4.1.9",
|
"react-timeago": "^4.1.9",
|
||||||
|
|
@ -32,6 +35,7 @@
|
||||||
"testformat": "prettier src/**/*.{ts,tsx} --list-different"
|
"testformat": "prettier src/**/*.{ts,tsx} --list-different"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/codemirror": "0.0.71",
|
||||||
"@types/detect-browser": "^2.0.1",
|
"@types/detect-browser": "^2.0.1",
|
||||||
"@types/get-port": "^4.0.0",
|
"@types/get-port": "^4.0.0",
|
||||||
"@types/jest": "^23.3.1",
|
"@types/jest": "^23.3.1",
|
||||||
|
|
@ -46,8 +50,8 @@
|
||||||
"get-port": "^4.0.0",
|
"get-port": "^4.0.0",
|
||||||
"prettier": "^1.14.2",
|
"prettier": "^1.14.2",
|
||||||
"puppeteer": "^1.8.0",
|
"puppeteer": "^1.8.0",
|
||||||
"rimraf": "^2.6.2",
|
|
||||||
"react-scripts-ts": "2.17.0",
|
"react-scripts-ts": "2.17.0",
|
||||||
|
"rimraf": "^2.6.2",
|
||||||
"tree-kill": "^1.2.0",
|
"tree-kill": "^1.2.0",
|
||||||
"tslint-sonarts": "^1.7.0",
|
"tslint-sonarts": "^1.7.0",
|
||||||
"typescript": "^3.0.1",
|
"typescript": "^3.0.1",
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export default class AddDialog extends Component<IProps, IState> {
|
||||||
margin="dense"
|
margin="dense"
|
||||||
className="name"
|
className="name"
|
||||||
label="Name *"
|
label="Name *"
|
||||||
type="email"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={this.handleChange.bind(this, 'name')}
|
onChange={this.handleChange.bind(this, 'name')}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,13 @@ export class AppStore extends BaseStore<IApplication> {
|
||||||
this.snack('Application image updated');
|
this.snack('Application image updated');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@action
|
||||||
|
public update = async (id: number, name: string, description: string): Promise<void> => {
|
||||||
|
await axios.put(`${config.get('url')}application/${id}`, {name, description});
|
||||||
|
await this.refresh();
|
||||||
|
this.snack('Application updated');
|
||||||
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
public create = async (name: string, description: string): Promise<void> => {
|
public create = async (name: string, description: string): Promise<void> => {
|
||||||
await axios.post(`${config.get('url')}application`, {name, description});
|
await axios.post(`${config.get('url')}application`, {name, description});
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import TableHead from '@material-ui/core/TableHead';
|
||||||
import TableRow from '@material-ui/core/TableRow';
|
import TableRow from '@material-ui/core/TableRow';
|
||||||
import Delete from '@material-ui/icons/Delete';
|
import Delete from '@material-ui/icons/Delete';
|
||||||
import Edit from '@material-ui/icons/Edit';
|
import Edit from '@material-ui/icons/Edit';
|
||||||
|
import CloudUpload from '@material-ui/icons/CloudUpload';
|
||||||
import React, {ChangeEvent, Component, SFC} from 'react';
|
import React, {ChangeEvent, Component, SFC} from 'react';
|
||||||
import ConfirmDialog from '../common/ConfirmDialog';
|
import ConfirmDialog from '../common/ConfirmDialog';
|
||||||
import DefaultPage from '../common/DefaultPage';
|
import DefaultPage from '../common/DefaultPage';
|
||||||
|
|
@ -17,12 +18,15 @@ import AddApplicationDialog from './AddApplicationDialog';
|
||||||
import {observer} from 'mobx-react';
|
import {observer} from 'mobx-react';
|
||||||
import {observable} from 'mobx';
|
import {observable} from 'mobx';
|
||||||
import {inject, Stores} from '../inject';
|
import {inject, Stores} from '../inject';
|
||||||
|
import UpdateDialog from './UpdateApplicationDialog';
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class Applications extends Component<Stores<'appStore'>> {
|
class Applications extends Component<Stores<'appStore'>> {
|
||||||
@observable
|
@observable
|
||||||
private deleteId: number | false = false;
|
private deleteId: number | false = false;
|
||||||
@observable
|
@observable
|
||||||
|
private updateId: number | false = false;
|
||||||
|
@observable
|
||||||
private createDialog = false;
|
private createDialog = false;
|
||||||
|
|
||||||
private uploadId = -1;
|
private uploadId = -1;
|
||||||
|
|
@ -34,6 +38,7 @@ class Applications extends Component<Stores<'appStore'>> {
|
||||||
const {
|
const {
|
||||||
createDialog,
|
createDialog,
|
||||||
deleteId,
|
deleteId,
|
||||||
|
updateId,
|
||||||
props: {appStore},
|
props: {appStore},
|
||||||
} = this;
|
} = this;
|
||||||
const apps = appStore.getItems();
|
const apps = appStore.getItems();
|
||||||
|
|
@ -54,6 +59,7 @@ class Applications extends Component<Stores<'appStore'>> {
|
||||||
<TableCell>Token</TableCell>
|
<TableCell>Token</TableCell>
|
||||||
<TableCell>Description</TableCell>
|
<TableCell>Description</TableCell>
|
||||||
<TableCell />
|
<TableCell />
|
||||||
|
<TableCell />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|
@ -67,6 +73,8 @@ class Applications extends Component<Stores<'appStore'>> {
|
||||||
value={app.token}
|
value={app.token}
|
||||||
fUpload={() => this.uploadImage(app.id)}
|
fUpload={() => this.uploadImage(app.id)}
|
||||||
fDelete={() => (this.deleteId = app.id)}
|
fDelete={() => (this.deleteId = app.id)}
|
||||||
|
fEdit={() => (this.updateId = app.id)}
|
||||||
|
noDelete={app.internal}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -86,6 +94,16 @@ class Applications extends Component<Stores<'appStore'>> {
|
||||||
fOnSubmit={appStore.create}
|
fOnSubmit={appStore.create}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{updateId !== false && (
|
||||||
|
<UpdateDialog
|
||||||
|
fClose={() => (this.updateId = false)}
|
||||||
|
fOnSubmit={(name, description) =>
|
||||||
|
appStore.update(updateId, name, description)
|
||||||
|
}
|
||||||
|
initialDescription={appStore.getByID(updateId).description}
|
||||||
|
initialName={appStore.getByID(updateId).name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{deleteId !== false && (
|
{deleteId !== false && (
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
title="Confirm Delete"
|
title="Confirm Delete"
|
||||||
|
|
@ -121,33 +139,42 @@ class Applications extends Component<Stores<'appStore'>> {
|
||||||
interface IRowProps {
|
interface IRowProps {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
noDelete: boolean;
|
||||||
description: string;
|
description: string;
|
||||||
fUpload: VoidFunction;
|
fUpload: VoidFunction;
|
||||||
image: string;
|
image: string;
|
||||||
fDelete: VoidFunction;
|
fDelete: VoidFunction;
|
||||||
|
fEdit: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Row: SFC<IRowProps> = observer(({name, value, description, fDelete, fUpload, image}) => (
|
const Row: SFC<IRowProps> = observer(
|
||||||
<TableRow>
|
({name, value, noDelete, description, fDelete, fUpload, image, fEdit}) => (
|
||||||
<TableCell padding="checkbox">
|
<TableRow>
|
||||||
<div style={{display: 'flex'}}>
|
<TableCell padding="checkbox">
|
||||||
<Avatar src={image} />
|
<div style={{display: 'flex'}}>
|
||||||
<IconButton onClick={fUpload} style={{height: 40}}>
|
<Avatar src={image} />
|
||||||
|
<IconButton onClick={fUpload} style={{height: 40}}>
|
||||||
|
<CloudUpload />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<ToggleVisibility value={value} style={{display: 'flex', alignItems: 'center'}} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{description}</TableCell>
|
||||||
|
<TableCell numeric padding="none">
|
||||||
|
<IconButton onClick={fEdit} className="edit">
|
||||||
<Edit />
|
<Edit />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell numeric padding="none">
|
||||||
<TableCell>{name}</TableCell>
|
<IconButton onClick={fDelete} className="delete" disabled={noDelete}>
|
||||||
<TableCell>
|
<Delete />
|
||||||
<ToggleVisibility value={value} style={{display: 'flex', alignItems: 'center'}} />
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{description}</TableCell>
|
</TableRow>
|
||||||
<TableCell numeric padding="none">
|
)
|
||||||
<IconButton onClick={fDelete} className="delete">
|
);
|
||||||
<Delete />
|
|
||||||
</IconButton>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
));
|
|
||||||
|
|
||||||
export default inject('appStore')(Applications);
|
export default inject('appStore')(Applications);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import Dialog from '@material-ui/core/Dialog';
|
||||||
|
import DialogActions from '@material-ui/core/DialogActions';
|
||||||
|
import DialogContent from '@material-ui/core/DialogContent';
|
||||||
|
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||||
|
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||||
|
import TextField from '@material-ui/core/TextField';
|
||||||
|
import Tooltip from '@material-ui/core/Tooltip';
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
fClose: VoidFunction;
|
||||||
|
fOnSubmit: (name: string, description: string) => void;
|
||||||
|
initialName: string;
|
||||||
|
initialDescription: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class UpdateDialog extends Component<IProps, IState> {
|
||||||
|
public state = {name: '', description: ''};
|
||||||
|
|
||||||
|
public componentWillMount() {
|
||||||
|
this.setState({name: this.props.initialName, description: this.props.initialDescription});
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const {fClose, fOnSubmit} = this.props;
|
||||||
|
const {name, description} = this.state;
|
||||||
|
const submitEnabled = this.state.name.length !== 0;
|
||||||
|
const submitAndClose = () => {
|
||||||
|
fOnSubmit(name, description);
|
||||||
|
fClose();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={true}
|
||||||
|
onClose={fClose}
|
||||||
|
aria-labelledby="form-dialog-title"
|
||||||
|
id="app-dialog">
|
||||||
|
<DialogTitle id="form-dialog-title">Update an application</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
An application is allowed to send messages.
|
||||||
|
</DialogContentText>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
className="name"
|
||||||
|
label="Name *"
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={this.handleChange.bind(this, 'name')}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
className="description"
|
||||||
|
label="Short Description"
|
||||||
|
value={description}
|
||||||
|
onChange={this.handleChange.bind(this, 'description')}
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={fClose}>Cancel</Button>
|
||||||
|
<Tooltip title={submitEnabled ? '' : 'name is required'}>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="update"
|
||||||
|
disabled={!submitEnabled}
|
||||||
|
onClick={submitAndClose}
|
||||||
|
color="primary"
|
||||||
|
variant="raised">
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleChange(propertyName: string, event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const state = this.state;
|
||||||
|
state[propertyName] = event.target.value;
|
||||||
|
this.setState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ import {InjectProvider, StoreMapping} from './inject';
|
||||||
import {UserStore} from './user/UserStore';
|
import {UserStore} from './user/UserStore';
|
||||||
import {MessagesStore} from './message/MessagesStore';
|
import {MessagesStore} from './message/MessagesStore';
|
||||||
import {ClientStore} from './client/ClientStore';
|
import {ClientStore} from './client/ClientStore';
|
||||||
|
import {PluginStore} from './plugin/PluginStore';
|
||||||
|
|
||||||
const defaultDevConfig = {
|
const defaultDevConfig = {
|
||||||
url: 'http://localhost:80/',
|
url: 'http://localhost:80/',
|
||||||
|
|
@ -22,7 +23,7 @@ const defaultDevConfig = {
|
||||||
|
|
||||||
const {port, hostname, protocol} = window.location;
|
const {port, hostname, protocol} = window.location;
|
||||||
const slashes = protocol.concat('//');
|
const slashes = protocol.concat('//');
|
||||||
const url = slashes.concat(hostname.concat(':', port));
|
const url = slashes.concat(port ? hostname.concat(':', port) : hostname);
|
||||||
const urlWithSlash = url.endsWith('/') ? url : url.concat('/');
|
const urlWithSlash = url.endsWith('/') ? url : url.concat('/');
|
||||||
|
|
||||||
const defaultProdConfig = {
|
const defaultProdConfig = {
|
||||||
|
|
@ -44,6 +45,7 @@ const initStores = (): StoreMapping => {
|
||||||
const currentUser = new CurrentUser(snackManager.snack);
|
const currentUser = new CurrentUser(snackManager.snack);
|
||||||
const clientStore = new ClientStore(snackManager.snack);
|
const clientStore = new ClientStore(snackManager.snack);
|
||||||
const wsStore = new WebSocketStore(snackManager.snack, currentUser);
|
const wsStore = new WebSocketStore(snackManager.snack, currentUser);
|
||||||
|
const pluginStore = new PluginStore(snackManager.snack);
|
||||||
appStore.onDelete = () => messagesStore.clearAll();
|
appStore.onDelete = () => messagesStore.clearAll();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -54,6 +56,7 @@ const initStores = (): StoreMapping => {
|
||||||
currentUser,
|
currentUser,
|
||||||
clientStore,
|
clientStore,
|
||||||
wsStore,
|
wsStore,
|
||||||
|
pluginStore,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {ClientStore} from './client/ClientStore';
|
||||||
import {AppStore} from './application/AppStore';
|
import {AppStore} from './application/AppStore';
|
||||||
import {inject as mobxInject, Provider} from 'mobx-react';
|
import {inject as mobxInject, Provider} from 'mobx-react';
|
||||||
import {WebSocketStore} from './message/WebSocketStore';
|
import {WebSocketStore} from './message/WebSocketStore';
|
||||||
|
import {PluginStore} from './plugin/PluginStore';
|
||||||
|
|
||||||
export interface StoreMapping {
|
export interface StoreMapping {
|
||||||
userStore: UserStore;
|
userStore: UserStore;
|
||||||
|
|
@ -15,6 +16,7 @@ export interface StoreMapping {
|
||||||
currentUser: CurrentUser;
|
currentUser: CurrentUser;
|
||||||
clientStore: ClientStore;
|
clientStore: ClientStore;
|
||||||
appStore: AppStore;
|
appStore: AppStore;
|
||||||
|
pluginStore: PluginStore;
|
||||||
wsStore: WebSocketStore;
|
wsStore: WebSocketStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import Chat from '@material-ui/icons/Chat';
|
||||||
import DevicesOther from '@material-ui/icons/DevicesOther';
|
import DevicesOther from '@material-ui/icons/DevicesOther';
|
||||||
import ExitToApp from '@material-ui/icons/ExitToApp';
|
import ExitToApp from '@material-ui/icons/ExitToApp';
|
||||||
import Highlight from '@material-ui/icons/Highlight';
|
import Highlight from '@material-ui/icons/Highlight';
|
||||||
|
import Apps from '@material-ui/icons/Apps';
|
||||||
import SupervisorAccount from '@material-ui/icons/SupervisorAccount';
|
import SupervisorAccount from '@material-ui/icons/SupervisorAccount';
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import {Link} from 'react-router-dom';
|
import {Link} from 'react-router-dom';
|
||||||
|
|
@ -104,6 +105,12 @@ class Header extends Component<IProps & Styles> {
|
||||||
clients
|
clients
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link className={classes.link} to="/plugins" id="navigate-plugins">
|
||||||
|
<Button color="inherit">
|
||||||
|
<Apps />
|
||||||
|
plugins
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
<Button color="inherit" onClick={showSettings} id="changepw">
|
<Button color="inherit" onClick={showSettings} id="changepw">
|
||||||
<AccountCircle />
|
<AccountCircle />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import SnackBarHandler from '../snack/SnackBarHandler';
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
import Applications from '../application/Applications';
|
import Applications from '../application/Applications';
|
||||||
import Clients from '../client/Clients';
|
import Clients from '../client/Clients';
|
||||||
|
import Plugins from '../plugin/Plugins';
|
||||||
|
import PluginDetailView from '../plugin/PluginDetailView';
|
||||||
import Login from '../user/Login';
|
import Login from '../user/Login';
|
||||||
import Messages from '../message/Messages';
|
import Messages from '../message/Messages';
|
||||||
import Users from '../user/Users';
|
import Users from '../user/Users';
|
||||||
|
|
@ -101,6 +103,8 @@ class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser
|
||||||
<Route exact path="/applications" component={Applications} />
|
<Route exact path="/applications" component={Applications} />
|
||||||
<Route exact path="/clients" component={Clients} />
|
<Route exact path="/clients" component={Clients} />
|
||||||
<Route exact path="/users" component={Users} />
|
<Route exact path="/users" component={Users} />
|
||||||
|
<Route exact path="/plugins" component={Plugins} />
|
||||||
|
<Route exact path="/plugins/:id" component={PluginDetailView} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</main>
|
</main>
|
||||||
{showSettings && (
|
{showSettings && (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,282 @@
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
import {RouteComponentProps} from 'react-router';
|
||||||
|
import ReactMarkDown from 'react-markdown';
|
||||||
|
import {UnControlled as CodeMirror} from 'react-codemirror2';
|
||||||
|
import 'codemirror/lib/codemirror.css';
|
||||||
|
import 'codemirror/theme/material.css';
|
||||||
|
import 'codemirror/mode/yaml/yaml';
|
||||||
|
import Info from '@material-ui/icons/Info';
|
||||||
|
import Build from '@material-ui/icons/Build';
|
||||||
|
import Subject from '@material-ui/icons/Subject';
|
||||||
|
import Refresh from '@material-ui/icons/Refresh';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import DefaultPage from '../common/DefaultPage';
|
||||||
|
import * as config from '../config';
|
||||||
|
import Container from '../common/Container';
|
||||||
|
import {inject, Stores} from '../inject';
|
||||||
|
|
||||||
|
interface IProps extends RouteComponentProps<{id: string}> {}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
displayText: string | null;
|
||||||
|
currentConfig: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluginDetailView extends Component<IProps & Stores<'pluginStore'>, IState> {
|
||||||
|
private pluginID: number = parseInt(this.props.match.params.id, 10);
|
||||||
|
private pluginInfo = () => this.props.pluginStore.getByID(this.pluginID);
|
||||||
|
|
||||||
|
public state: IState = {
|
||||||
|
displayText: null,
|
||||||
|
currentConfig: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
public componentWillMount() {
|
||||||
|
this.refreshFeatures();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillReceiveProps(nextProps: IProps & Stores<'pluginStore'>) {
|
||||||
|
this.pluginID = parseInt(nextProps.match.params.id, 10);
|
||||||
|
this.refreshFeatures();
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshFeatures() {
|
||||||
|
return Promise.all([this.refreshConfigurer(), this.refreshDisplayer()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshConfigurer() {
|
||||||
|
const {
|
||||||
|
props: {pluginStore},
|
||||||
|
} = this;
|
||||||
|
if (this.pluginInfo().capabilities.indexOf('configurer') !== -1) {
|
||||||
|
const response = await pluginStore.requestConfig(this.pluginID);
|
||||||
|
this.setState({currentConfig: response});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshDisplayer() {
|
||||||
|
const {
|
||||||
|
props: {pluginStore},
|
||||||
|
} = this;
|
||||||
|
if (this.pluginInfo().capabilities.indexOf('displayer') !== -1) {
|
||||||
|
const response = await pluginStore.requestDisplay(this.pluginID);
|
||||||
|
this.setState({displayText: response});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const pluginInfo = this.pluginInfo();
|
||||||
|
const {name, capabilities} = pluginInfo;
|
||||||
|
return (
|
||||||
|
<DefaultPage title={name} hideButton={true} maxWidth={1000}>
|
||||||
|
<PanelWrapper name={'Plugin Info'} icon={Info}>
|
||||||
|
<PluginInfo pluginInfo={pluginInfo} />
|
||||||
|
</PanelWrapper>
|
||||||
|
{capabilities.indexOf('configurer') !== -1 ? (
|
||||||
|
<PanelWrapper
|
||||||
|
name={'Configurer'}
|
||||||
|
description={'This is the configuration panel for this plugin.'}
|
||||||
|
icon={Build}
|
||||||
|
refresh={this.refreshConfigurer.bind(this)}>
|
||||||
|
<ConfigurerPanel
|
||||||
|
pluginInfo={pluginInfo}
|
||||||
|
initialConfig={
|
||||||
|
this.state.currentConfig !== null
|
||||||
|
? this.state.currentConfig
|
||||||
|
: 'Loading...'
|
||||||
|
}
|
||||||
|
save={async (newConfig) => {
|
||||||
|
await this.props.pluginStore.changeConfig(this.pluginID, newConfig);
|
||||||
|
await this.refreshFeatures();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PanelWrapper>
|
||||||
|
) : null}{' '}
|
||||||
|
{capabilities.indexOf('displayer') !== -1 ? (
|
||||||
|
<PanelWrapper
|
||||||
|
name={'Displayer'}
|
||||||
|
description={'This is the information generated by the plugin.'}
|
||||||
|
refresh={this.refreshDisplayer.bind(this)}
|
||||||
|
icon={Subject}>
|
||||||
|
<DisplayerPanel
|
||||||
|
pluginInfo={pluginInfo}
|
||||||
|
displayText={
|
||||||
|
this.state.displayText !== null
|
||||||
|
? this.state.displayText
|
||||||
|
: 'Loading...'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PanelWrapper>
|
||||||
|
) : null}
|
||||||
|
</DefaultPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPanelWrapperProps {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
refresh?: () => Promise<void>;
|
||||||
|
icon?: React.ComponentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PanelWrapper: React.SFC<IPanelWrapperProps> = ({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
refresh,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const Icon = icon;
|
||||||
|
return (
|
||||||
|
<Container style={{display: 'block', width: '100%', margin: '20px', color: 'white'}}>
|
||||||
|
<Typography variant="headline">
|
||||||
|
{Icon ? (
|
||||||
|
<span>
|
||||||
|
<Icon />
|
||||||
|
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{name}
|
||||||
|
{refresh ? (
|
||||||
|
<Button
|
||||||
|
style={{float: 'right'}}
|
||||||
|
onClick={() => {
|
||||||
|
refresh();
|
||||||
|
}}>
|
||||||
|
<Refresh />
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Typography>
|
||||||
|
{description ? (
|
||||||
|
<Typography variant="subheading" style={{color: 'grey'}}>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
<hr />
|
||||||
|
<div
|
||||||
|
className={name
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/ /g, '-')}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IConfigurerPanelProps {
|
||||||
|
pluginInfo: IPlugin;
|
||||||
|
initialConfig: string;
|
||||||
|
save: (newConfig: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
class ConfigurerPanel extends Component<IConfigurerPanelProps, {unsavedChanges: string | null}> {
|
||||||
|
public state = {unsavedChanges: null};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CodeMirror
|
||||||
|
value={this.props.initialConfig}
|
||||||
|
options={{
|
||||||
|
mode: 'yaml',
|
||||||
|
theme: 'material',
|
||||||
|
lineNumbers: true,
|
||||||
|
}}
|
||||||
|
onChange={(instance, data, value) => {
|
||||||
|
let newConf: string | null = value;
|
||||||
|
if (value === this.props.initialConfig) {
|
||||||
|
newConf = null;
|
||||||
|
}
|
||||||
|
this.setState({unsavedChanges: newConf});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
fullWidth={true}
|
||||||
|
disabled={
|
||||||
|
this.state.unsavedChanges === null ||
|
||||||
|
this.state.unsavedChanges === this.props.initialConfig
|
||||||
|
}
|
||||||
|
className="config-save"
|
||||||
|
onClick={() => {
|
||||||
|
const newConfig = this.state.unsavedChanges;
|
||||||
|
this.props.save(newConfig!).then(() => {
|
||||||
|
this.setState({unsavedChanges: ''});
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<Typography variant="button">Save</Typography>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDisplayerPanelProps {
|
||||||
|
pluginInfo: IPlugin;
|
||||||
|
displayText: string;
|
||||||
|
}
|
||||||
|
const DisplayerPanel: React.SFC<IDisplayerPanelProps> = ({pluginInfo, displayText}) => (
|
||||||
|
<Typography variant="body2">
|
||||||
|
<ReactMarkDown source={displayText} />
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
|
||||||
|
class PluginInfo extends Component<{pluginInfo: IPlugin}> {
|
||||||
|
public render() {
|
||||||
|
const {
|
||||||
|
props: {
|
||||||
|
pluginInfo: {name, author, modulePath, website, license, capabilities, id, token},
|
||||||
|
},
|
||||||
|
} = this;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{name ? (
|
||||||
|
<Typography variant="body2" className="name">
|
||||||
|
Name: <span>{name}</span>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
{author ? (
|
||||||
|
<Typography variant="body2" className="author">
|
||||||
|
Author: <span>{author}</span>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
<Typography variant="body2" className="module-path">
|
||||||
|
Module Path: <span>{modulePath}</span>
|
||||||
|
</Typography>
|
||||||
|
{website ? (
|
||||||
|
<Typography variant="body2" className="website">
|
||||||
|
Website: <span>{website}</span>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
{license ? (
|
||||||
|
<Typography variant="body2" className="license">
|
||||||
|
License: <span>{license}</span>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
<Typography variant="body2" className="capabilities">
|
||||||
|
Capabilities: <span>{capabilities.join(', ')}</span>
|
||||||
|
</Typography>
|
||||||
|
{capabilities.indexOf('webhooker') !== -1 ? (
|
||||||
|
<Typography variant="body2">
|
||||||
|
Custom Route Prefix:{' '}
|
||||||
|
{((url) => (
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
style={{color: 'white'}}
|
||||||
|
className="custom-route">
|
||||||
|
{url}
|
||||||
|
</a>
|
||||||
|
))(`${config.get('url')}plugin/${id}/custom/${token}/`)}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default inject('pluginStore')(PluginDetailView);
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import {action} from 'mobx';
|
||||||
|
import {BaseStore} from '../common/BaseStore';
|
||||||
|
import * as config from '../config';
|
||||||
|
import {SnackReporter} from '../snack/SnackManager';
|
||||||
|
|
||||||
|
export class PluginStore extends BaseStore<IPlugin> {
|
||||||
|
public onDelete: () => void = () => {};
|
||||||
|
|
||||||
|
public constructor(private readonly snack: SnackReporter) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public requestConfig = (id: number): Promise<string> => {
|
||||||
|
return axios
|
||||||
|
.get(`${config.get('url')}plugin/${id}/config`)
|
||||||
|
.then((response) => response.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
public requestDisplay = (id: number): Promise<string> => {
|
||||||
|
return axios
|
||||||
|
.get(`${config.get('url')}plugin/${id}/display`)
|
||||||
|
.then((response) => response.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
protected requestItems = (): Promise<IPlugin[]> => {
|
||||||
|
return axios.get<IPlugin[]>(`${config.get('url')}plugin`).then((response) => response.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
protected requestDelete = (id: number): Promise<void> => {
|
||||||
|
this.snack('Cannot delete plugin');
|
||||||
|
throw new Error('Cannot delete plugin');
|
||||||
|
};
|
||||||
|
|
||||||
|
public getName = (id: number): string => {
|
||||||
|
const plugin = this.getByIDOrUndefined(id);
|
||||||
|
return id === -1 ? 'All Plugins' : plugin !== undefined ? plugin.name : 'unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
@action
|
||||||
|
public changeConfig = async (id: number, newConfig: string): Promise<void> => {
|
||||||
|
await axios.post(`${config.get('url')}plugin/${id}/config`, newConfig, {
|
||||||
|
headers: {'content-type': 'application/x-yaml'},
|
||||||
|
});
|
||||||
|
this.snack(`Plugin config updated`);
|
||||||
|
await this.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
@action
|
||||||
|
public changeEnabledState = async (id: number, enabled: boolean): Promise<void> => {
|
||||||
|
await axios.post(`${config.get('url')}plugin/${id}/${enabled ? 'enable' : 'disable'}`);
|
||||||
|
this.snack(`Plugin ${enabled ? 'enabled' : 'disabled'}`);
|
||||||
|
await this.refresh();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
import React, {Component, SFC} from 'react';
|
||||||
|
import {Link} from 'react-router-dom';
|
||||||
|
import Grid from '@material-ui/core/Grid';
|
||||||
|
import Paper from '@material-ui/core/Paper';
|
||||||
|
import Table from '@material-ui/core/Table';
|
||||||
|
import TableBody from '@material-ui/core/TableBody';
|
||||||
|
import TableCell from '@material-ui/core/TableCell';
|
||||||
|
import TableHead from '@material-ui/core/TableHead';
|
||||||
|
import TableRow from '@material-ui/core/TableRow';
|
||||||
|
import Settings from '@material-ui/icons/Settings';
|
||||||
|
import {Switch, Button} from '@material-ui/core';
|
||||||
|
import DefaultPage from '../common/DefaultPage';
|
||||||
|
import ToggleVisibility from '../common/ToggleVisibility';
|
||||||
|
import {observer} from 'mobx-react';
|
||||||
|
import {inject, Stores} from '../inject';
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class Plugins extends Component<Stores<'pluginStore'>> {
|
||||||
|
public componentDidMount = () => this.props.pluginStore.refresh();
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const {
|
||||||
|
props: {pluginStore},
|
||||||
|
} = this;
|
||||||
|
const plugins = pluginStore.getItems();
|
||||||
|
return (
|
||||||
|
<DefaultPage title="Plugins" hideButton={true} maxWidth={1000}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Paper elevation={6}>
|
||||||
|
<Table id="plugin-table">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>ID</TableCell>
|
||||||
|
<TableCell>Enabled</TableCell>
|
||||||
|
<TableCell>Name</TableCell>
|
||||||
|
<TableCell>Token</TableCell>
|
||||||
|
<TableCell>Details</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{plugins.map((plugin: IPlugin) => {
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
key={plugin.token}
|
||||||
|
id={plugin.id}
|
||||||
|
token={plugin.token}
|
||||||
|
name={plugin.name}
|
||||||
|
enabled={plugin.enabled}
|
||||||
|
fToggleStatus={() =>
|
||||||
|
this.props.pluginStore.changeEnabledState(
|
||||||
|
plugin.id,
|
||||||
|
!plugin.enabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
</DefaultPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRowProps {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
token: string;
|
||||||
|
enabled: boolean;
|
||||||
|
fToggleStatus: VoidFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Row: SFC<IRowProps> = observer(({name, id, token, enabled, fToggleStatus}) => (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>{id}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Switch
|
||||||
|
checked={enabled}
|
||||||
|
onClick={fToggleStatus}
|
||||||
|
className="switch"
|
||||||
|
data-enabled={enabled}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<ToggleVisibility value={token} style={{display: 'flex', alignItems: 'center'}} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell numeric padding="none">
|
||||||
|
<Link to={'/plugins/' + id}>
|
||||||
|
<Button>
|
||||||
|
<Settings />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
));
|
||||||
|
|
||||||
|
export default inject('pluginStore')(Plugins);
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import {Page} from 'puppeteer';
|
import {Page} from 'puppeteer';
|
||||||
import {newTest, GotifyTest} from './setup';
|
import {newTest, GotifyTest} from './setup';
|
||||||
import {count, innerText, waitForExists, waitToDisappear} from './utils';
|
import {count, innerText, waitForExists, waitToDisappear, clearField} from './utils';
|
||||||
import * as auth from './authentication';
|
import * as auth from './authentication';
|
||||||
import * as selector from './selector';
|
import * as selector from './selector';
|
||||||
|
|
||||||
|
|
@ -17,7 +17,8 @@ enum Col {
|
||||||
Name = 2,
|
Name = 2,
|
||||||
Token = 3,
|
Token = 3,
|
||||||
Description = 4,
|
Description = 4,
|
||||||
EditDelete = 5,
|
EditUpdate = 5,
|
||||||
|
EditDelete = 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
const hiddenToken = '•••••••••••••••';
|
const hiddenToken = '•••••••••••••••';
|
||||||
|
|
@ -25,6 +26,36 @@ const hiddenToken = '•••••••••••••••';
|
||||||
const $table = selector.table('#app-table');
|
const $table = selector.table('#app-table');
|
||||||
const $dialog = selector.form('#app-dialog');
|
const $dialog = selector.form('#app-dialog');
|
||||||
|
|
||||||
|
const hasApp = (name: string, description: string, row: number): (() => Promise<void>) => {
|
||||||
|
return async () => {
|
||||||
|
expect(await innerText(page, $table.cell(row, Col.Name))).toBe(name);
|
||||||
|
expect(await innerText(page, $table.cell(row, Col.Token))).toBe(hiddenToken);
|
||||||
|
expect(await innerText(page, $table.cell(row, Col.Description))).toBe(description);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateApp = (
|
||||||
|
id: number,
|
||||||
|
data: {name?: string; description?: string}
|
||||||
|
): (() => Promise<void>) => {
|
||||||
|
return async () => {
|
||||||
|
await page.click($table.cell(id, Col.EditUpdate, '.edit'));
|
||||||
|
await page.waitForSelector($dialog.selector());
|
||||||
|
if (data.name) {
|
||||||
|
const nameSelector = $dialog.input('.name');
|
||||||
|
await clearField(page, nameSelector);
|
||||||
|
await page.type(nameSelector, data.name);
|
||||||
|
}
|
||||||
|
if (data.description) {
|
||||||
|
const descSelector = $dialog.textarea('.description');
|
||||||
|
await clearField(page, descSelector);
|
||||||
|
await page.type(descSelector, data.description);
|
||||||
|
}
|
||||||
|
await page.click($dialog.button('.update'));
|
||||||
|
await waitToDisappear(page, $dialog.selector());
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const createApp = (name: string, description: string): (() => Promise<void>) => {
|
export const createApp = (name: string, description: string): (() => Promise<void>) => {
|
||||||
return async () => {
|
return async () => {
|
||||||
await page.click('#create-app');
|
await page.click('#create-app');
|
||||||
|
|
@ -53,14 +84,6 @@ describe('Application', () => {
|
||||||
it('raspberry', createApp('raspberry', '#3'));
|
it('raspberry', createApp('raspberry', '#3'));
|
||||||
});
|
});
|
||||||
describe('has created apps', () => {
|
describe('has created apps', () => {
|
||||||
const hasApp = (name: string, description: string, row: number): (() => Promise<void>) => {
|
|
||||||
return async () => {
|
|
||||||
expect(await innerText(page, $table.cell(row, Col.Name))).toBe(name);
|
|
||||||
expect(await innerText(page, $table.cell(row, Col.Token))).toBe(hiddenToken);
|
|
||||||
expect(await innerText(page, $table.cell(row, Col.Description))).toBe(description);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
it('has three apps', async () => {
|
it('has three apps', async () => {
|
||||||
await page.waitForSelector($table.row(3));
|
await page.waitForSelector($table.row(3));
|
||||||
expect(await count(page, $table.rows())).toBe(3);
|
expect(await count(page, $table.rows())).toBe(3);
|
||||||
|
|
@ -72,8 +95,19 @@ describe('Application', () => {
|
||||||
await page.click($table.cell(3, Col.Token, '.toggle-visibility'));
|
await page.click($table.cell(3, Col.Token, '.toggle-visibility'));
|
||||||
const token = await innerText(page, $table.cell(3, Col.Token));
|
const token = await innerText(page, $table.cell(3, Col.Token));
|
||||||
expect(token.startsWith('A')).toBeTruthy();
|
expect(token.startsWith('A')).toBeTruthy();
|
||||||
|
await page.click($table.cell(3, Col.Token, '.toggle-visibility'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('updates application', async () => {
|
||||||
|
await updateApp(1, {name: 'server_linux'})();
|
||||||
|
await updateApp(2, {description: 'kitchen_computer'})();
|
||||||
|
await updateApp(3, {name: 'raspberry_pi', description: 'home_pi'})();
|
||||||
|
});
|
||||||
|
it('has updated application', async () => {
|
||||||
|
await hasApp('server_linux', '#1', 1)();
|
||||||
|
await hasApp('desktop', 'kitchen_computer', 2)();
|
||||||
|
await hasApp('raspberry_pi', 'home_pi', 3)();
|
||||||
|
});
|
||||||
it('deletes application', async () => {
|
it('deletes application', async () => {
|
||||||
await page.click($table.cell(2, Col.EditDelete, '.delete'));
|
await page.click($table.cell(2, Col.EditDelete, '.delete'));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
import * as os from 'os';
|
||||||
|
import {Page} from 'puppeteer';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import * as auth from './authentication';
|
||||||
|
import * as selector from './selector';
|
||||||
|
import {GotifyTest, newTest, newPluginDir} from './setup';
|
||||||
|
import {count, innerText, waitForExists} from './utils';
|
||||||
|
|
||||||
|
const pluginSupported = ['linux', 'darwin'].indexOf(os.platform()) !== -1;
|
||||||
|
|
||||||
|
let page: Page;
|
||||||
|
let gotify: GotifyTest;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const gotifyPluginDir = pluginSupported
|
||||||
|
? await newPluginDir(['github.com/gotify/server/plugin/example/echo'])
|
||||||
|
: '';
|
||||||
|
gotify = await newTest(gotifyPluginDir);
|
||||||
|
page = gotify.page;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => await gotify.close());
|
||||||
|
|
||||||
|
enum Col {
|
||||||
|
ID = 1,
|
||||||
|
SetEnabled = 2,
|
||||||
|
Name = 3,
|
||||||
|
Token = 4,
|
||||||
|
Details = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
const hiddenToken = '•••••••••••••••';
|
||||||
|
|
||||||
|
const $table = selector.table('#plugin-table');
|
||||||
|
|
||||||
|
const switchSelctor = (id: number) => $table.cell(id, Col.SetEnabled, '[data-enabled]');
|
||||||
|
|
||||||
|
const enabledState = async (id: number) =>
|
||||||
|
(await page.$eval(switchSelctor(id), (el) => el.getAttribute('data-enabled'))) === 'true';
|
||||||
|
|
||||||
|
const toggleEnabled = async (id: number) => {
|
||||||
|
const origEnabled = await enabledState(id).toString();
|
||||||
|
await page.click(switchSelctor(id));
|
||||||
|
await page.waitForFunction(
|
||||||
|
`document.querySelector("${switchSelctor(
|
||||||
|
id
|
||||||
|
)}").getAttribute("data-enabled") !== "${origEnabled}"`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pluginInfo = async (className: string) => {
|
||||||
|
return await innerText(page, `.plugin-info .${className} > span`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDisplayer = async () => {
|
||||||
|
return await innerText(page, '.displayer');
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasReceivedMessage = async (title: RegExp, content: RegExp) => {
|
||||||
|
await page.click('#message-navigation a');
|
||||||
|
await waitForExists(page, selector.heading(), 'All Messages');
|
||||||
|
|
||||||
|
expect(await innerText(page, '.title')).toMatch(title);
|
||||||
|
expect(await innerText(page, '.content')).toMatch(content);
|
||||||
|
|
||||||
|
await page.click('#navigate-plugins');
|
||||||
|
await waitForExists(page, selector.heading(), 'Plugins');
|
||||||
|
};
|
||||||
|
|
||||||
|
const inDetailPage = async (id: number, callback: (() => Promise<void>)) => {
|
||||||
|
const name = await innerText(page, $table.cell(id, Col.Name));
|
||||||
|
await page.click($table.cell(id, Col.Details));
|
||||||
|
await waitForExists(page, '.plugin-info .name > span', name);
|
||||||
|
|
||||||
|
await callback();
|
||||||
|
|
||||||
|
await page.click('#navigate-plugins');
|
||||||
|
await waitForExists(page, selector.heading(), 'Plugins');
|
||||||
|
await page.waitForSelector($table.selector());
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('plugin', () => {
|
||||||
|
describe('navigation', () => {
|
||||||
|
it('does login', async () => await auth.login(page));
|
||||||
|
it('navigates to plugins', async () => {
|
||||||
|
await page.click('#navigate-plugins');
|
||||||
|
await waitForExists(page, selector.heading(), 'Plugins');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!pluginSupported) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
describe('functionality test', () => {
|
||||||
|
describe('initial status', () => {
|
||||||
|
it('has echo plugin', async () => {
|
||||||
|
expect(await count(page, $table.rows())).toBe(1);
|
||||||
|
expect(await innerText(page, $table.cell(1, Col.Name))).toEqual('test plugin');
|
||||||
|
expect(await innerText(page, $table.cell(1, Col.Token))).toBe(hiddenToken);
|
||||||
|
expect(parseInt(await innerText(page, $table.cell(1, Col.ID)), 10)).toBeGreaterThan(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('is disabled by default', async () => {
|
||||||
|
expect(await enabledState(1)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('enable and disable plugin', () => {
|
||||||
|
it('enable', async () => {
|
||||||
|
await toggleEnabled(1);
|
||||||
|
expect(await enabledState(1)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disable', async () => {
|
||||||
|
await toggleEnabled(1);
|
||||||
|
expect(await enabledState(1)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('details page', () => {
|
||||||
|
it('has plugin info', async () => {
|
||||||
|
await inDetailPage(1, async () => {
|
||||||
|
expect(await pluginInfo('module-path')).toBe(
|
||||||
|
'github.com/gotify/server/plugin/example/echo'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('has displayer', async () => {
|
||||||
|
await inDetailPage(1, async () => {
|
||||||
|
expect(await getDisplayer()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('has configurer', async () => {
|
||||||
|
await inDetailPage(1, async () => {
|
||||||
|
expect(await page.$('.configurer')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('updates configurer', async () => {
|
||||||
|
await inDetailPage(1, async () => {
|
||||||
|
expect(
|
||||||
|
await (await (await page.$('.config-save'))!.getProperty(
|
||||||
|
'disabled'
|
||||||
|
)).jsonValue()
|
||||||
|
).toBe(true);
|
||||||
|
await page.waitForSelector('.CodeMirror .CodeMirror-code');
|
||||||
|
await page.waitForFunction(
|
||||||
|
'document.querySelector(".CodeMirror .CodeMirror-code").innerText.toLowerCase().indexOf("loading")<0'
|
||||||
|
);
|
||||||
|
await page.click('.CodeMirror .CodeMirror-code > div');
|
||||||
|
await page.keyboard.press('x');
|
||||||
|
await page.waitForFunction(
|
||||||
|
'document.querySelector(".config-save") && !document.querySelector(".config-save").disabled'
|
||||||
|
);
|
||||||
|
await page.click('.config-save');
|
||||||
|
await page.waitForFunction('document.querySelector(".config-save").disabled');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('configurer updated', async () => {
|
||||||
|
await inDetailPage(1, async () => {
|
||||||
|
expect(
|
||||||
|
await (await (await page.$('.config-save'))!.getProperty(
|
||||||
|
'disabled'
|
||||||
|
)).jsonValue()
|
||||||
|
).toBe(true);
|
||||||
|
await page.waitForSelector('.CodeMirror .CodeMirror-code > div');
|
||||||
|
await page.waitForFunction(
|
||||||
|
'document.querySelector(".CodeMirror .CodeMirror-code > div").innerText.toLowerCase().indexOf("loading")<0'
|
||||||
|
);
|
||||||
|
expect(await innerText(page, '.CodeMirror .CodeMirror-code > div')).toMatch(
|
||||||
|
/x$/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('sends messages', async () => {
|
||||||
|
if (!(await enabledState(1))) {
|
||||||
|
await toggleEnabled(1);
|
||||||
|
}
|
||||||
|
await inDetailPage(1, async () => {
|
||||||
|
const hook = await page.$eval('.displayer a', (el) => el.getAttribute('href'));
|
||||||
|
await axios.get(hook!);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('has received message', async () => {
|
||||||
|
await hasReceivedMessage(
|
||||||
|
/^.+received$/,
|
||||||
|
/^echo server received a hello message \d+ times$/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -19,14 +19,22 @@ const windowsPrefix = process.platform === 'win32' ? '.exe' : '';
|
||||||
const appDotGo = path.join(__dirname, '..', '..', '..', 'app.go');
|
const appDotGo = path.join(__dirname, '..', '..', '..', 'app.go');
|
||||||
const testBuildPath = path.join(__dirname, 'build');
|
const testBuildPath = path.join(__dirname, 'build');
|
||||||
|
|
||||||
export const newTest = async (): Promise<GotifyTest> => {
|
export const newPluginDir = async (plugins: string[]): Promise<string> => {
|
||||||
|
const {dir, generator} = testPluginDir();
|
||||||
|
for (const pluginName of plugins) {
|
||||||
|
await buildGoPlugin(generator(), pluginName);
|
||||||
|
}
|
||||||
|
return dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const newTest = async (pluginsDir = ''): Promise<GotifyTest> => {
|
||||||
const port = await getPort();
|
const port = await getPort();
|
||||||
|
|
||||||
const gotifyFile = testFilePath();
|
const gotifyFile = testFilePath();
|
||||||
|
|
||||||
await buildGoExecutable(gotifyFile);
|
await buildGoExecutable(gotifyFile);
|
||||||
|
|
||||||
const gotifyInstance = startGotify(gotifyFile, port);
|
const gotifyInstance = startGotify(gotifyFile, port, pluginsDir);
|
||||||
|
|
||||||
const gotifyURL = 'http://localhost:' + port;
|
const gotifyURL = 'http://localhost:' + port;
|
||||||
await waitForGotify('http-get://localhost:' + port);
|
await waitForGotify('http-get://localhost:' + port);
|
||||||
|
|
@ -52,6 +60,26 @@ export const newTest = async (): Promise<GotifyTest> => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testPluginDir = (): {dir: string; generator: (() => string)} => {
|
||||||
|
const random = Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substring(2, 15);
|
||||||
|
const dirName = 'gotifyplugin_' + random;
|
||||||
|
const dir = path.join(testBuildPath, dirName);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, 0o755);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
dir,
|
||||||
|
generator: () => {
|
||||||
|
const randomFn = Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substring(2, 15);
|
||||||
|
return path.join(dir, randomFn + '.so');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const testFilePath = (): string => {
|
const testFilePath = (): string => {
|
||||||
const random = Math.random()
|
const random = Math.random()
|
||||||
.toString(36)
|
.toString(36)
|
||||||
|
|
@ -73,6 +101,13 @@ const waitForGotify = (url: string): Promise<void> => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildGoPlugin = (filename: string, pluginPath: string): Promise<void> => {
|
||||||
|
process.stdout.write(`### Building Plugin ${pluginPath}\n`);
|
||||||
|
return new Promise((resolve) =>
|
||||||
|
exec(`go build -o ${filename} -buildmode=plugin ${pluginPath}`, () => resolve())
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const buildGoExecutable = (filename: string): Promise<void> => {
|
const buildGoExecutable = (filename: string): Promise<void> => {
|
||||||
const envGotify = process.env.GOTIFY_EXE;
|
const envGotify = process.env.GOTIFY_EXE;
|
||||||
if (envGotify) {
|
if (envGotify) {
|
||||||
|
|
@ -90,11 +125,12 @@ const buildGoExecutable = (filename: string): Promise<void> => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const startGotify = (filename: string, port: number): ChildProcess => {
|
const startGotify = (filename: string, port: number, pluginDir: string): ChildProcess => {
|
||||||
const gotify = spawn(filename, [], {
|
const gotify = spawn(filename, [], {
|
||||||
env: {
|
env: {
|
||||||
GOTIFY_SERVER_PORT: '' + port,
|
GOTIFY_SERVER_PORT: '' + port,
|
||||||
GOTIFY_DATABASE_CONNECTION: 'file::memory:?mode=memory&cache=shared',
|
GOTIFY_DATABASE_CONNECTION: 'file::memory:?mode=memory&cache=shared',
|
||||||
|
GOTIFY_PLUGINSDIR: pluginDir,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
gotify.stdout.pipe(process.stdout);
|
gotify.stdout.pipe(process.stdout);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ interface IApplication {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
image: string;
|
image: string;
|
||||||
|
internal: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IClient {
|
interface IClient {
|
||||||
|
|
@ -12,6 +13,18 @@ interface IClient {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IPlugin {
|
||||||
|
id: number;
|
||||||
|
token: string;
|
||||||
|
name: string;
|
||||||
|
modulePath: string;
|
||||||
|
enabled: boolean;
|
||||||
|
author?: string;
|
||||||
|
website?: string;
|
||||||
|
license?: string;
|
||||||
|
capabilities: Array<'webhooker' | 'displayer' | 'configurer' | 'messenger' | 'storager'>;
|
||||||
|
}
|
||||||
|
|
||||||
interface IMessage {
|
interface IMessage {
|
||||||
id: number;
|
id: number;
|
||||||
appid: number;
|
appid: number;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue