mirror of
https://github.com/mainflux/mainflux.git
synced 2025-04-26 13:48:53 +08:00
MF-164 - Split manager service (#266)
This commit is contained in:
parent
6a361209c8
commit
816c172823
104
Gopkg.lock
generated
104
Gopkg.lock
generated
@ -96,6 +96,7 @@
|
||||
"metrics",
|
||||
"metrics/internal/lv",
|
||||
"metrics/prometheus",
|
||||
"transport/grpc",
|
||||
"transport/http"
|
||||
]
|
||||
revision = "4dc7be5d2d12881735283bcab7352178e190fc71"
|
||||
@ -127,7 +128,13 @@
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
packages = [
|
||||
"proto",
|
||||
"ptypes",
|
||||
"ptypes/any",
|
||||
"ptypes/duration",
|
||||
"ptypes/timestamp"
|
||||
]
|
||||
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
|
||||
version = "v1.1.0"
|
||||
|
||||
@ -323,6 +330,31 @@
|
||||
revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/atomic"
|
||||
packages = ["."]
|
||||
revision = "8474b86a5a6f79c443ce4b2992817ff32cf208b8"
|
||||
version = "v1.3.1"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/multierr"
|
||||
packages = ["."]
|
||||
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/zap"
|
||||
packages = [
|
||||
".",
|
||||
"buffer",
|
||||
"internal/bufferpool",
|
||||
"internal/color",
|
||||
"internal/exit",
|
||||
"zapcore"
|
||||
]
|
||||
revision = "eeedf312bc6c57391d84767a4cd413f02a917974"
|
||||
version = "v1.8.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
@ -336,7 +368,16 @@
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = ["context"]
|
||||
packages = [
|
||||
"context",
|
||||
"http/httpguts",
|
||||
"http2",
|
||||
"http2/hpack",
|
||||
"idna",
|
||||
"internal/timeseries",
|
||||
"lex/httplex",
|
||||
"trace"
|
||||
]
|
||||
revision = "5f9ae10d9af5b1c89ae6904293b14b064d4ada23"
|
||||
|
||||
[[projects]]
|
||||
@ -348,6 +389,63 @@
|
||||
]
|
||||
revision = "78d5f264b493f125018180c204871ecf58a2dce1"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"collate",
|
||||
"collate/build",
|
||||
"internal/colltab",
|
||||
"internal/gen",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"language",
|
||||
"secure/bidirule",
|
||||
"transform",
|
||||
"unicode/bidi",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"unicode/rangetable"
|
||||
]
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "google.golang.org/genproto"
|
||||
packages = ["googleapis/rpc/status"]
|
||||
revision = "86e600f69ee4704c6efbf6a2a40a5c10700e76c2"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/grpc"
|
||||
packages = [
|
||||
".",
|
||||
"balancer",
|
||||
"balancer/base",
|
||||
"balancer/roundrobin",
|
||||
"codes",
|
||||
"connectivity",
|
||||
"credentials",
|
||||
"encoding",
|
||||
"encoding/proto",
|
||||
"grpclb/grpc_lb_v1/messages",
|
||||
"grpclog",
|
||||
"internal",
|
||||
"keepalive",
|
||||
"metadata",
|
||||
"naming",
|
||||
"peer",
|
||||
"resolver",
|
||||
"resolver/dns",
|
||||
"resolver/passthrough",
|
||||
"stats",
|
||||
"status",
|
||||
"tap",
|
||||
"transport"
|
||||
]
|
||||
revision = "d11072e7ca9811b1100b80ca0269ac831f06d024"
|
||||
version = "v1.11.3"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
packages = ["."]
|
||||
@ -357,6 +455,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "e78b70d1d03d916583038763dfca6960e7c449af8734b32ded5642090d120d7f"
|
||||
inputs-digest = "7c4092dbae8a1935ea4743156e6afe332ccce03e42bb1552804766440acb56e2"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
12
Makefile
12
Makefile
@ -1,5 +1,5 @@
|
||||
BUILD_DIR = build
|
||||
SERVICES = manager http normalizer ws coap
|
||||
SERVICES = users clients http normalizer ws coap
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
GOOS ?= linux
|
||||
@ -23,7 +23,7 @@ install:
|
||||
cp ${BUILD_DIR}/* $(GOBIN)
|
||||
|
||||
proto:
|
||||
protoc --go_out=. *.proto
|
||||
protoc --go_out=plugins=grpc:. *.proto
|
||||
|
||||
$(SERVICES): proto
|
||||
$(call compile_service,$(@))
|
||||
@ -32,8 +32,7 @@ $(DOCKERS):
|
||||
$(call make_docker,$(@))
|
||||
|
||||
dockers: $(DOCKERS)
|
||||
docker build --tag=mainflux/dashflux -f ./dashflux/docker/Dockerfile ./dashflux
|
||||
|
||||
docker build --tag=mainflux/dashflux -f dashflux/docker/Dockerfile dashflux
|
||||
|
||||
latest: dockers
|
||||
for svc in $(SERVICES); do \
|
||||
@ -41,7 +40,6 @@ latest: dockers
|
||||
done
|
||||
docker push mainflux/dashflux
|
||||
|
||||
|
||||
release:
|
||||
$(eval version = $(shell git describe --abbrev=0 --tags))
|
||||
git checkout $(version)
|
||||
@ -50,5 +48,5 @@ release:
|
||||
docker tag mainflux/$$svc mainflux/$$svc:$(version); \
|
||||
docker push mainflux/$$svc:$(version); \
|
||||
done
|
||||
docker tag mainflux/dashflux mainflux/dashflux:$(version); \
|
||||
docker push mainflux/dashflux:$(version); \
|
||||
docker tag mainflux/dashflux mainflux/dashflux:$(version)
|
||||
docker push mainflux/dashflux:$(version)
|
||||
|
13
api.go
Normal file
13
api.go
Normal file
@ -0,0 +1,13 @@
|
||||
package mainflux
|
||||
|
||||
// Response contains HTTP response specifig methods.
|
||||
type Response interface {
|
||||
// Code returns HTTP response code.
|
||||
Code() int
|
||||
|
||||
// Headers returns map of HTTP headers with their values.
|
||||
Headers() map[string]string
|
||||
|
||||
// Empty indicates if HTTP response has content.
|
||||
Empty() bool
|
||||
}
|
81
clients/README.md
Normal file
81
clients/README.md
Normal file
@ -0,0 +1,81 @@
|
||||
# Clients
|
||||
|
||||
Clients service provides an HTTP API for managing platform resources: devices,
|
||||
applications and channels. Through this API clients are able to do the following
|
||||
actions:
|
||||
|
||||
- provision new clients (i.e. devices & applications)
|
||||
- create new channels
|
||||
- "connect" clients into the channels
|
||||
|
||||
For in-depth explanation of the aforementioned scenarios, as well as thorough
|
||||
understanding of Mainflux, please check out the [official documentation][doc].
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the
|
||||
following table. Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|------------------------|------------------------------------------|----------------|
|
||||
| MF_CLIENTS_DB_HOST | Database host address | localhost |
|
||||
| MF_CLIENTS_DB_PORT | Database host port | 5432 |
|
||||
| MF_CLIENTS_DB_USER | Database user | mainflux |
|
||||
| MF_CLIENTS_DB_PASSWORD | Database password | mainflux |
|
||||
| MF_CLIENTS_DB | Name of the database used by the service | clients |
|
||||
| MF_CLIENTS_HTTP_PORT | Clients service HTTP port | 8180 |
|
||||
| MF_CLIENTS_GRPC_PORT | Clients service gRPC port | 8181 |
|
||||
| MF_USERS_URL | Users service URL | localhost:8181 |
|
||||
| MF_CLIENTS_SECRET | String used for signing tokens | clients |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. The following snippet
|
||||
provides a compose file template that can be used to deploy the service container
|
||||
locally:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
services:
|
||||
clients:
|
||||
image: mainflux/clients:[version]
|
||||
container_name: [instance name]
|
||||
ports:
|
||||
- [host machine port]:[configured HTTP port]
|
||||
environment:
|
||||
MF_CLIENTS_DB_HOST: [Database host address]
|
||||
MF_CLIENTS_DB_PORT: [Database host port]
|
||||
MF_CLIENTS_DB_USER: [Database user]
|
||||
MF_CLIENTS_DB_PASS: [Database password]
|
||||
MF_CLIENTS_DB: [Name of the database used by the service]
|
||||
MF_CLIENTS_HTTP_PORT: [Service HTTP port]
|
||||
MF_CLIENTS_GRPC_PORT: [Service gRPC port]
|
||||
MF_USERS_URL: [Users service URL]
|
||||
MF_CLIENTS_SECRET: [String used for signing tokens]
|
||||
```
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
go get github.com/mainflux/mainflux
|
||||
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux
|
||||
|
||||
# compile the clients
|
||||
make clients
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_CLIENTS_DB_HOST=[Database host address] MF_CLIENTS_DB_PORT=[Database host port] MF_CLIENTS_DB_USER=[Database user] MF_CLIENTS_DB_PASS=[Database password] MF_CLIENTS_DB=[Name of the database used by the service] MF_CLIENTS_HTTP_PORT=[Service HTTP port] MF_CLIENTS_GRPC_PORT=[Service gRPC port] MF_USERS_URL=[Users service URL] MF_CLIENTS_SECRET=[String used for signing tokens] $GOBIN/mainflux-clients
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](swagger.yaml).
|
||||
|
||||
[doc]: http://mainflux.readthedocs.io
|
49
clients/api/grpc/client.go
Normal file
49
clients/api/grpc/client.go
Normal file
@ -0,0 +1,49 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"github.com/mainflux/mainflux"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.ClientsServiceClient = (*grpcClient)(nil)
|
||||
|
||||
type grpcClient struct {
|
||||
canAccess endpoint.Endpoint
|
||||
}
|
||||
|
||||
// NewClient returns new gRPC client instance.
|
||||
func NewClient(conn *grpc.ClientConn) mainflux.ClientsServiceClient {
|
||||
endpoint := kitgrpc.NewClient(
|
||||
conn,
|
||||
"mainflux.ClientsService",
|
||||
"CanAccess",
|
||||
encodeCanAccessRequest,
|
||||
decodeCanAccessResponse,
|
||||
mainflux.Identity{},
|
||||
).Endpoint()
|
||||
|
||||
return &grpcClient{endpoint}
|
||||
}
|
||||
|
||||
func (client grpcClient) CanAccess(ctx context.Context, req *mainflux.AccessReq, _ ...grpc.CallOption) (*mainflux.Identity, error) {
|
||||
res, err := client.canAccess(ctx, accessReq{req.GetToken(), req.GetChanID()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ar := res.(accessRes)
|
||||
return &mainflux.Identity{Value: ar.id}, ar.err
|
||||
}
|
||||
|
||||
func encodeCanAccessRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(accessReq)
|
||||
return &mainflux.AccessReq{Token: req.clientKey, ChanID: req.chanID}, nil
|
||||
}
|
||||
|
||||
func decodeCanAccessResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.Identity)
|
||||
return accessRes{res.GetValue(), nil}, nil
|
||||
}
|
2
clients/api/grpc/doc.go
Normal file
2
clients/api/grpc/doc.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package grpc contains implementation of clients service gRPC API.
|
||||
package grpc
|
22
clients/api/grpc/endpoint.go
Normal file
22
clients/api/grpc/endpoint.go
Normal file
@ -0,0 +1,22 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
context "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func canAccessEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(accessReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := svc.CanAccess(req.clientKey, req.chanID)
|
||||
if err != nil {
|
||||
return accessRes{"", err}, err
|
||||
}
|
||||
return accessRes{id, nil}, nil
|
||||
}
|
||||
}
|
85
clients/api/grpc/endpoint_test.go
Normal file
85
clients/api/grpc/endpoint_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
grpcapi "github.com/mainflux/mainflux/clients/api/grpc"
|
||||
"github.com/mainflux/mainflux/clients/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8080
|
||||
token = "token"
|
||||
email = "john.doe@email.com"
|
||||
)
|
||||
|
||||
var (
|
||||
client = clients.Client{Type: "app", Name: "test_app", Payload: "test_payload"}
|
||||
channel = clients.Channel{Name: "test"}
|
||||
errMalformedReq = status.Error(codes.InvalidArgument, "received invalid can access request")
|
||||
errUnauthorizedAccess = status.Error(codes.PermissionDenied, "failed to identify client or client isn't connected to specified channel")
|
||||
)
|
||||
|
||||
func newService(tokens map[string]string) clients.Service {
|
||||
users := mocks.NewUsersService(tokens)
|
||||
clientsRepo := mocks.NewClientRepository()
|
||||
channelsRepo := mocks.NewChannelRepository(clientsRepo)
|
||||
hasher := mocks.NewHasher()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
|
||||
return clients.New(users, clientsRepo, channelsRepo, hasher, idp)
|
||||
}
|
||||
|
||||
func startGRPCServer(svc clients.Service, port int) {
|
||||
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
server := grpc.NewServer()
|
||||
mainflux.RegisterClientsServiceServer(server, grpcapi.NewServer(svc))
|
||||
go server.Serve(listener)
|
||||
}
|
||||
|
||||
func TestCanAccess(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
startGRPCServer(svc, port)
|
||||
|
||||
connectedClientID, _ := svc.AddClient(token, client)
|
||||
connectedClient, _ := svc.ViewClient(token, connectedClientID)
|
||||
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
client, _ := svc.ViewClient(token, clientID)
|
||||
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
svc.Connect(token, chanID, connectedClientID)
|
||||
|
||||
usersAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(usersAddr, grpc.WithInsecure())
|
||||
cli := grpcapi.NewClient(conn)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
cases := map[string]struct {
|
||||
clientKey string
|
||||
chanID string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
"check if connected client can access existing channel": {connectedClient.Key, chanID, connectedClientID, nil},
|
||||
"check if unconnected client can access existing channel": {client.Key, chanID, "", errUnauthorizedAccess},
|
||||
"check if connected client can access non-existent channel": {connectedClient.Key, "1", "", errMalformedReq},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
id, err := cli.CanAccess(ctx, &mainflux.AccessReq{tc.clientKey, tc.chanID})
|
||||
assert.Equal(t, tc.id, id.GetValue(), fmt.Sprintf("%s: expected %s got %s", desc, tc.id, id.GetValue()))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err))
|
||||
}
|
||||
}
|
18
clients/api/grpc/requests.go
Normal file
18
clients/api/grpc/requests.go
Normal file
@ -0,0 +1,18 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
type accessReq struct {
|
||||
clientKey string
|
||||
chanID string
|
||||
}
|
||||
|
||||
func (req accessReq) validate() error {
|
||||
if !govalidator.IsUUID(req.chanID) || req.clientKey == "" {
|
||||
return clients.ErrMalformedEntity
|
||||
}
|
||||
return nil
|
||||
}
|
6
clients/api/grpc/responses.go
Normal file
6
clients/api/grpc/responses.go
Normal file
@ -0,0 +1,6 @@
|
||||
package grpc
|
||||
|
||||
type accessRes struct {
|
||||
id string
|
||||
err error
|
||||
}
|
59
clients/api/grpc/server.go
Normal file
59
clients/api/grpc/server.go
Normal file
@ -0,0 +1,59 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var _ mainflux.ClientsServiceServer = (*grpcServer)(nil)
|
||||
|
||||
type grpcServer struct {
|
||||
handler kitgrpc.Handler
|
||||
}
|
||||
|
||||
// NewServer returns new ClientsServiceServer instance.
|
||||
func NewServer(svc clients.Service) mainflux.ClientsServiceServer {
|
||||
handler := kitgrpc.NewServer(
|
||||
canAccessEndpoint(svc),
|
||||
decodeCanAccessRequest,
|
||||
encodeCanAccessResponse,
|
||||
)
|
||||
return &grpcServer{handler}
|
||||
}
|
||||
|
||||
func (s *grpcServer) CanAccess(ctx context.Context, req *mainflux.AccessReq) (*mainflux.Identity, error) {
|
||||
_, res, err := s.handler.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.Identity), nil
|
||||
}
|
||||
|
||||
func decodeCanAccessRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.AccessReq)
|
||||
return accessReq{req.GetToken(), req.GetChanID()}, nil
|
||||
}
|
||||
|
||||
func encodeCanAccessResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(accessRes)
|
||||
return &mainflux.Identity{Value: res.id}, encodeError(res.err)
|
||||
}
|
||||
|
||||
func encodeError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch err {
|
||||
case clients.ErrMalformedEntity:
|
||||
return status.Error(codes.InvalidArgument, "received invalid can access request")
|
||||
case clients.ErrUnauthorizedAccess:
|
||||
return status.Error(codes.PermissionDenied, "failed to identify client or client isn't connected to specified channel")
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
}
|
2
clients/api/http/doc.go
Normal file
2
clients/api/http/doc.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package http contains implementation of clients service HTTP API.
|
||||
package http
|
@ -1,43 +1,13 @@
|
||||
package api
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
func registrationEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(userReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err := svc.Register(req.user)
|
||||
return tokenRes{}, err
|
||||
}
|
||||
}
|
||||
|
||||
func loginEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(userReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := svc.Login(req.user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tokenRes{token}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func addClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func addClientEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(addClientReq)
|
||||
|
||||
@ -54,7 +24,7 @@ func addClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func updateClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func updateClientEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateClientReq)
|
||||
|
||||
@ -72,7 +42,7 @@ func updateClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func viewClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func viewClientEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
@ -89,7 +59,7 @@ func viewClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func listClientsEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func listClientsEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listResourcesReq)
|
||||
|
||||
@ -106,12 +76,12 @@ func listClientsEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func removeClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func removeClientEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
err := req.validate()
|
||||
if err == manager.ErrNotFound {
|
||||
if err == clients.ErrNotFound {
|
||||
return removeRes{}, nil
|
||||
}
|
||||
|
||||
@ -127,7 +97,7 @@ func removeClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func createChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func createChannelEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(createChannelReq)
|
||||
|
||||
@ -144,7 +114,7 @@ func createChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func updateChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func updateChannelEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateChannelReq)
|
||||
|
||||
@ -162,7 +132,7 @@ func updateChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func viewChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func viewChannelEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
@ -179,7 +149,7 @@ func viewChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func listChannelsEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func listChannelsEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listResourcesReq)
|
||||
|
||||
@ -196,12 +166,12 @@ func listChannelsEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func removeChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func removeChannelEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
if err == manager.ErrNotFound {
|
||||
if err == clients.ErrNotFound {
|
||||
return removeRes{}, nil
|
||||
}
|
||||
return nil, err
|
||||
@ -214,7 +184,7 @@ func removeChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return removeRes{}, nil
|
||||
}
|
||||
}
|
||||
func connectEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func connectEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
cr := request.(connectionReq)
|
||||
|
||||
@ -222,7 +192,7 @@ func connectEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Connect(cr.key, cr.chanId, cr.clientId); err != nil {
|
||||
if err := svc.Connect(cr.key, cr.chanID, cr.clientID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -230,7 +200,7 @@ func connectEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func disconnectEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func disconnectEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
cr := request.(connectionReq)
|
||||
|
||||
@ -238,44 +208,10 @@ func disconnectEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Disconnect(cr.key, cr.chanId, cr.clientId); err != nil {
|
||||
if err := svc.Disconnect(cr.key, cr.chanID, cr.clientID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return disconnectionRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func identityEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(identityReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
id, err := svc.Identity(req.key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return identityRes{id: id}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func canAccessEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
id, err := svc.CanAccess(req.key, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return identityRes{id: id}, nil
|
||||
}
|
||||
}
|
610
clients/api/http/endpoint_test.go
Normal file
610
clients/api/http/endpoint_test.go
Normal file
@ -0,0 +1,610 @@
|
||||
package http_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
httpapi "github.com/mainflux/mainflux/clients/api/http"
|
||||
"github.com/mainflux/mainflux/clients/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
invalidEmail = "userexample.com"
|
||||
email = "user@example.com"
|
||||
token = "token"
|
||||
invalidToken = "invalid_token"
|
||||
wrongID = "123e4567-e89b-12d3-a456-000000000042"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
)
|
||||
|
||||
var (
|
||||
client = clients.Client{Type: "app", Name: "test_app", Payload: "test_payload"}
|
||||
channel = clients.Channel{Name: "test"}
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService(tokens map[string]string) clients.Service {
|
||||
users := mocks.NewUsersService(tokens)
|
||||
clientsRepo := mocks.NewClientRepository()
|
||||
channelsRepo := mocks.NewChannelRepository(clientsRepo)
|
||||
hasher := mocks.NewHasher()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
|
||||
return clients.New(users, clientsRepo, channelsRepo, hasher, idp)
|
||||
}
|
||||
|
||||
func newServer(svc clients.Service) *httptest.Server {
|
||||
mux := httpapi.MakeHandler(svc)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func TestAddClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
data := toJSON(client)
|
||||
invalidData := toJSON(clients.Client{
|
||||
Type: "foo",
|
||||
Name: "invalid_client",
|
||||
Payload: "some_payload",
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
location string
|
||||
}{
|
||||
{"add valid client", data, contentType, token, http.StatusCreated, fmt.Sprintf("/clients/%s", id)},
|
||||
{"add client with invalid data", invalidData, contentType, token, http.StatusBadRequest, ""},
|
||||
{"add client with invalid auth token", data, contentType, invalidToken, http.StatusForbidden, ""},
|
||||
{"add client with invalid request format", "}", contentType, token, http.StatusBadRequest, ""},
|
||||
{"add client with empty JSON request", "{}", contentType, token, http.StatusBadRequest, ""},
|
||||
{"add client with empty request", "", contentType, token, http.StatusBadRequest, ""},
|
||||
{"add client with missing content type", data, "", token, http.StatusUnsupportedMediaType, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/clients", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
location := res.Header.Get("Location")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.location, location, fmt.Sprintf("%s: expected location %s got %s", tc.desc, tc.location, location))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
data := toJSON(client)
|
||||
invalidData := toJSON(clients.Client{
|
||||
Type: "foo",
|
||||
Name: client.Name,
|
||||
Payload: client.Payload,
|
||||
})
|
||||
id, _ := svc.AddClient(token, client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
id string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"update existing client", data, id, contentType, token, http.StatusOK},
|
||||
{"update non-existent client", data, wrongID, contentType, token, http.StatusNotFound},
|
||||
{"update client with invalid id", data, "1", contentType, token, http.StatusNotFound},
|
||||
{"update client with invalid data", invalidData, id, contentType, token, http.StatusBadRequest},
|
||||
{"update client with invalid user token", data, id, contentType, invalidToken, http.StatusForbidden},
|
||||
{"update client with invalid data format", "{", id, contentType, token, http.StatusBadRequest},
|
||||
{"update client with empty JSON request", "{}", id, contentType, token, http.StatusBadRequest},
|
||||
{"update client with empty request", "", id, contentType, token, http.StatusBadRequest},
|
||||
{"update client with missing content type", data, id, "", token, http.StatusUnsupportedMediaType},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
fmt.Println(req.url)
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
id, _ := svc.AddClient(token, client)
|
||||
|
||||
client.ID = id
|
||||
client.Key = id
|
||||
data := toJSON(client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
res string
|
||||
}{
|
||||
{"view existing client", id, token, http.StatusOK, data},
|
||||
{"view non-existent client", wrongID, token, http.StatusNotFound, ""},
|
||||
{"view client by passing invalid id", "1", token, http.StatusNotFound, ""},
|
||||
{"view client by passing invalid token", id, invalidToken, http.StatusForbidden, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
data := strings.Trim(string(body), "\n")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.res, data, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListClients(t *testing.T) {
|
||||
noClientsToken := "no_clients_token"
|
||||
svc := newService(map[string]string{
|
||||
token: email,
|
||||
noClientsToken: "no_clients_user@example.com",
|
||||
})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
data := []clients.Client{}
|
||||
for i := 0; i < 101; i++ {
|
||||
id, _ := svc.AddClient(token, client)
|
||||
client.ID = id
|
||||
client.Key = id
|
||||
data = append(data, client)
|
||||
}
|
||||
clientURL := fmt.Sprintf("%s/clients", ts.URL)
|
||||
cases := []struct {
|
||||
desc string
|
||||
auth string
|
||||
status int
|
||||
url string
|
||||
res []clients.Client
|
||||
}{
|
||||
{"get a list of clients", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 5), data[0:5]},
|
||||
{"get a list of clients with invalid token", invalidToken, http.StatusForbidden, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 1), nil},
|
||||
{"get a list of clients with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, -1, 5), nil},
|
||||
{"get a list of clients with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 1, -5), nil},
|
||||
{"get a list of clients with zero limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 1, 0), nil},
|
||||
{"get a list of clients with no offset provided", token, http.StatusOK, fmt.Sprintf("%s?limit=%d", clientURL, 5), data[0:5]},
|
||||
{"get a list of clients with no limit provided", token, http.StatusOK, fmt.Sprintf("%s?offset=%d", clientURL, 1), data[1:11]},
|
||||
{"get a list of clients with redundant query params", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d&value=something", clientURL, 0, 5), data[0:5]},
|
||||
{"get a list of clients with limit greater than max", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 110), nil},
|
||||
{"get a list of clients with default URL", token, http.StatusOK, fmt.Sprintf("%s%s", clientURL, ""), data[0:10]},
|
||||
{"get a list of clients with invalid URL", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?%%"), nil},
|
||||
{"get a list of clients with invalid number of params", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=4&limit=4&limit=5&offset=5"), nil},
|
||||
{"get a list of clients with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=e&limit=5"), nil},
|
||||
{"get a list of clients with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=5&limit=e"), nil},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: tc.url,
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
var data map[string][]clients.Client
|
||||
json.NewDecoder(res.Body).Decode(&data)
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.ElementsMatch(t, tc.res, data["clients"], fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data["clients"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
id, _ := svc.AddClient(token, client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"delete existing client", id, token, http.StatusNoContent},
|
||||
{"delete non-existent client", wrongID, token, http.StatusNoContent},
|
||||
{"delete client with invalid id", "1", token, http.StatusNoContent},
|
||||
{"delete client with invalid token", id, invalidToken, http.StatusForbidden},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
data := toJSON(channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
location string
|
||||
}{
|
||||
{"create new channel", data, contentType, token, http.StatusCreated, fmt.Sprintf("/channels/%s", id)},
|
||||
{"create new channel with invalid token", data, contentType, invalidToken, http.StatusForbidden, ""},
|
||||
{"create new channel with invalid data format", "{", contentType, token, http.StatusBadRequest, ""},
|
||||
{"create new channel with empty JSON request", "{}", contentType, token, http.StatusCreated, "/channels/123e4567-e89b-12d3-a456-000000000002"},
|
||||
{"create new channel with empty request", "", contentType, token, http.StatusBadRequest, ""},
|
||||
{"create new channel with missing content type", data, "", token, http.StatusUnsupportedMediaType, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/channels", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
location := res.Header.Get("Location")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.location, location, fmt.Sprintf("%s: expected location %s got %s", tc.desc, tc.location, location))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
updateData := toJSON(map[string]string{
|
||||
"name": "updated_channel",
|
||||
})
|
||||
id, _ := svc.CreateChannel(token, channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
id string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"update existing channel", updateData, id, contentType, token, http.StatusOK},
|
||||
{"update non-existing channel", updateData, wrongID, contentType, token, http.StatusNotFound},
|
||||
{"update channel with invalid token", updateData, id, contentType, invalidToken, http.StatusForbidden},
|
||||
{"update channel with invalid id", updateData, "1", contentType, token, http.StatusNotFound},
|
||||
{"update channel with invalid data format", "}", id, contentType, token, http.StatusBadRequest},
|
||||
{"update channel with empty JSON object", "{}", id, contentType, token, http.StatusOK},
|
||||
{"update channel with empty request", "", id, contentType, token, http.StatusBadRequest},
|
||||
{"update channel with missing content type", updateData, id, "", token, http.StatusUnsupportedMediaType},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
id, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = id
|
||||
data := toJSON(channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
res string
|
||||
}{
|
||||
{"view existing channel", id, token, http.StatusOK, data},
|
||||
{"view non-existent channel", wrongID, token, http.StatusNotFound, ""},
|
||||
{"view channel with invalid id", "1", token, http.StatusNotFound, ""},
|
||||
{"view channel with invalid token", id, invalidToken, http.StatusForbidden, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
body := strings.Trim(string(data), "\n")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.res, body, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListChannels(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
channels := []clients.Channel{}
|
||||
for i := 0; i < 101; i++ {
|
||||
id, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = id
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
channelURL := fmt.Sprintf("%s/channels", ts.URL)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
auth string
|
||||
status int
|
||||
url string
|
||||
res []clients.Channel
|
||||
}{
|
||||
{"get a list of channels", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 6), channels[0:6]},
|
||||
{"get a list of channels with invalid token", invalidToken, http.StatusForbidden, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 1), nil},
|
||||
{"get a list of channels with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, -1, 5), nil},
|
||||
{"get a list of channels with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, -1, 5), nil},
|
||||
{"get a list of channels with zero limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 1, 0), nil},
|
||||
{"get a list of channels with no offset provided", token, http.StatusOK, fmt.Sprintf("%s?limit=%d", channelURL, 5), channels[0:5]},
|
||||
{"get a list of channels with no limit provided", token, http.StatusOK, fmt.Sprintf("%s?offset=%d", channelURL, 1), channels[1:11]},
|
||||
{"get a list of channels with redundant query params", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d&value=something", channelURL, 0, 5), channels[0:5]},
|
||||
{"get a list of channels with limit greater than max", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 110), nil},
|
||||
{"get a list of channels with default URL", token, http.StatusOK, fmt.Sprintf("%s%s", channelURL, ""), channels[0:10]},
|
||||
{"get a list of channels with invalid URL", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?%%"), nil},
|
||||
{"get a list of channels with invalid number of params", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=4&limit=4&limit=5&offset=5"), nil},
|
||||
{"get a list of channels with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=e&limit=5"), nil},
|
||||
{"get a list of channels with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=5&limit=e"), nil},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: tc.url,
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
var body map[string][]clients.Channel
|
||||
json.NewDecoder(res.Body).Decode(&body)
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.ElementsMatch(t, tc.res, body["channels"], fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, body["channels"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
id, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = id
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"remove existing channel", channel.ID, token, http.StatusNoContent},
|
||||
{"remove non-existent channel", channel.ID, token, http.StatusNoContent},
|
||||
{"remove channel with invalid id", wrongID, token, http.StatusNoContent},
|
||||
{"remove channel with invalid token", channel.ID, "invalidToken", http.StatusForbidden},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
otherToken := "other_token"
|
||||
otherEmail := "other_user@example.com"
|
||||
svc := newService(map[string]string{
|
||||
token: email,
|
||||
otherToken: otherEmail,
|
||||
})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
|
||||
otherClientID, _ := svc.AddClient(otherToken, client)
|
||||
otherChanID, _ := svc.CreateChannel(otherToken, channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chanID string
|
||||
clientID string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"connect existing client to existing channel", chanID, clientID, token, http.StatusOK},
|
||||
{"connect existing client to non-existent channel", wrongID, clientID, token, http.StatusNotFound},
|
||||
{"connect client with invalid id to channel", chanID, "1", token, http.StatusNotFound},
|
||||
{"connect client to channel with invalid id", "1", clientID, token, http.StatusNotFound},
|
||||
{"connect existing client to existing channel with invalid token", chanID, clientID, invalidToken, http.StatusForbidden},
|
||||
{"connect client from owner to channel of other user", otherChanID, clientID, token, http.StatusNotFound},
|
||||
{"connect client from other user to owner's channel", chanID, otherClientID, token, http.StatusNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/channels/%s/clients/%s", ts.URL, tc.chanID, tc.clientID),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnnect(t *testing.T) {
|
||||
otherToken := "other_token"
|
||||
otherEmail := "other_user@example.com"
|
||||
svc := newService(map[string]string{
|
||||
token: email,
|
||||
otherToken: otherEmail,
|
||||
})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
svc.Connect(token, chanID, clientID)
|
||||
otherClientID, _ := svc.AddClient(otherToken, client)
|
||||
otherChanID, _ := svc.CreateChannel(otherToken, channel)
|
||||
svc.Connect(otherToken, otherChanID, otherClientID)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chanID string
|
||||
clientID string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"disconnect connected client from channel", chanID, clientID, token, http.StatusNoContent},
|
||||
{"disconnect non-connected client from channel", chanID, clientID, token, http.StatusNotFound},
|
||||
{"disconnect non-existent client from channel", chanID, "1", token, http.StatusNotFound},
|
||||
{"disconnect client from non-existent channel", "1", clientID, token, http.StatusNotFound},
|
||||
{"disconnect client from channel with invalid token", chanID, clientID, invalidToken, http.StatusForbidden},
|
||||
{"disconnect owner's client from someone elses channel", otherChanID, clientID, token, http.StatusNotFound},
|
||||
{"disconnect other's client from owner's channel", chanID, otherClientID, token, http.StatusNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/channels/%s/clients/%s", ts.URL, tc.chanID, tc.clientID),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package api
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
const maxLimitSize = 100
|
||||
@ -11,21 +11,13 @@ type apiReq interface {
|
||||
validate() error
|
||||
}
|
||||
|
||||
type userReq struct {
|
||||
user manager.User
|
||||
}
|
||||
|
||||
func (req userReq) validate() error {
|
||||
return req.user.Validate()
|
||||
}
|
||||
|
||||
type identityReq struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (req identityReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -33,12 +25,12 @@ func (req identityReq) validate() error {
|
||||
|
||||
type addClientReq struct {
|
||||
key string
|
||||
client manager.Client
|
||||
client clients.Client
|
||||
}
|
||||
|
||||
func (req addClientReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return req.client.Validate()
|
||||
@ -47,16 +39,16 @@ func (req addClientReq) validate() error {
|
||||
type updateClientReq struct {
|
||||
key string
|
||||
id string
|
||||
client manager.Client
|
||||
client clients.Client
|
||||
}
|
||||
|
||||
func (req updateClientReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return manager.ErrNotFound
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return req.client.Validate()
|
||||
@ -64,12 +56,12 @@ func (req updateClientReq) validate() error {
|
||||
|
||||
type createChannelReq struct {
|
||||
key string
|
||||
channel manager.Channel
|
||||
channel clients.Channel
|
||||
}
|
||||
|
||||
func (req createChannelReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -78,16 +70,16 @@ func (req createChannelReq) validate() error {
|
||||
type updateChannelReq struct {
|
||||
key string
|
||||
id string
|
||||
channel manager.Channel
|
||||
channel clients.Channel
|
||||
}
|
||||
|
||||
func (req updateChannelReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return manager.ErrNotFound
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -100,11 +92,11 @@ type viewResourceReq struct {
|
||||
|
||||
func (req viewResourceReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return manager.ErrNotFound
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -118,29 +110,29 @@ type listResourcesReq struct {
|
||||
|
||||
func (req *listResourcesReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.offset >= 0 && req.limit > 0 && req.limit <= maxLimitSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
return manager.ErrMalformedEntity
|
||||
return clients.ErrMalformedEntity
|
||||
}
|
||||
|
||||
type connectionReq struct {
|
||||
key string
|
||||
chanId string
|
||||
clientId string
|
||||
chanID string
|
||||
clientID string
|
||||
}
|
||||
|
||||
func (req connectionReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.chanId) || !govalidator.IsUUID(req.clientId) {
|
||||
return manager.ErrNotFound
|
||||
if !govalidator.IsUUID(req.chanID) || !govalidator.IsUUID(req.clientID) {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
@ -1,10 +1,10 @@
|
||||
package api
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@ -12,35 +12,17 @@ import (
|
||||
const wrong string = "?"
|
||||
|
||||
var (
|
||||
client = manager.Client{Type: "app"}
|
||||
channel = manager.Channel{}
|
||||
client = clients.Client{Type: "app"}
|
||||
channel = clients.Channel{}
|
||||
)
|
||||
|
||||
func TestUserReqValidation(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
user manager.User
|
||||
err error
|
||||
}{
|
||||
"valid user request": {manager.User{"foo@example.com", "pass"}, nil},
|
||||
"malformed e-mail": {manager.User{wrong, "pass"}, manager.ErrMalformedEntity},
|
||||
"empty e-mail": {manager.User{"", "pass"}, manager.ErrMalformedEntity},
|
||||
"empty password": {manager.User{"foo@example.com", ""}, manager.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := userReq{tc.user}
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityReqValidation(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"non-empty token": {uuid.NewV4().String(), nil},
|
||||
"empty token": {"", manager.ErrUnauthorizedAccess},
|
||||
"empty token": {"", clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@ -54,13 +36,13 @@ func TestAddClientReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
client clients.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid client addition request": {client, key, nil},
|
||||
"missing token": {client, "", manager.ErrUnauthorizedAccess},
|
||||
"wrong client type": {manager.Client{Type: wrong}, key, manager.ErrMalformedEntity},
|
||||
"missing token": {client, "", clients.ErrUnauthorizedAccess},
|
||||
"wrong client type": {clients.Client{Type: wrong}, key, clients.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@ -79,15 +61,15 @@ func TestUpdateClientReqValidation(t *testing.T) {
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
client clients.Client
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid client update request": {client, id, key, nil},
|
||||
"non-uuid client ID": {client, wrong, key, manager.ErrNotFound},
|
||||
"missing token": {client, id, "", manager.ErrUnauthorizedAccess},
|
||||
"wrong client type": {manager.Client{Type: "invalid"}, id, key, manager.ErrMalformedEntity},
|
||||
"non-uuid client ID": {client, wrong, key, clients.ErrNotFound},
|
||||
"missing token": {client, id, "", clients.ErrUnauthorizedAccess},
|
||||
"wrong client type": {clients.Client{Type: "invalid"}, id, key, clients.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@ -106,12 +88,12 @@ func TestCreateChannelReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
channel clients.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid channel creation request": {channel, key, nil},
|
||||
"missing token": {channel, "", manager.ErrUnauthorizedAccess},
|
||||
"missing token": {channel, "", clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@ -130,14 +112,14 @@ func TestUpdateChannelReqValidation(t *testing.T) {
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
channel clients.Channel
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid channel update request": {channel, id, key, nil},
|
||||
"non-uuid channel ID": {channel, wrong, key, manager.ErrNotFound},
|
||||
"missing token": {channel, id, "", manager.ErrUnauthorizedAccess},
|
||||
"non-uuid channel ID": {channel, wrong, key, clients.ErrNotFound},
|
||||
"missing token": {channel, id, "", clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@ -162,8 +144,8 @@ func TestViewResourceReqValidation(t *testing.T) {
|
||||
err error
|
||||
}{
|
||||
"valid resource viewing request": {id, key, nil},
|
||||
"missing token": {id, "", manager.ErrUnauthorizedAccess},
|
||||
"non-uuid resource ID": {wrong, key, manager.ErrNotFound},
|
||||
"missing token": {id, "", clients.ErrUnauthorizedAccess},
|
||||
"non-uuid resource ID": {wrong, key, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@ -184,11 +166,11 @@ func TestListResourcesReqValidation(t *testing.T) {
|
||||
err error
|
||||
}{
|
||||
"valid listing request": {key, value, value, nil},
|
||||
"missing token": {"", value, value, manager.ErrUnauthorizedAccess},
|
||||
"negative offset": {key, -value, value, manager.ErrMalformedEntity},
|
||||
"zero limit": {key, value, 0, manager.ErrMalformedEntity},
|
||||
"negative limit": {key, value, -value, manager.ErrMalformedEntity},
|
||||
"too big limit": {key, value, 20 * value, manager.ErrMalformedEntity},
|
||||
"missing token": {"", value, value, clients.ErrUnauthorizedAccess},
|
||||
"negative offset": {key, -value, value, clients.ErrMalformedEntity},
|
||||
"zero limit": {key, value, 0, clients.ErrMalformedEntity},
|
||||
"negative limit": {key, value, -value, clients.ErrMalformedEntity},
|
||||
"too big limit": {key, value, 20 * value, clients.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
201
clients/api/http/responses.go
Normal file
201
clients/api/http/responses.go
Normal file
@ -0,0 +1,201 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var (
|
||||
_ mainflux.Response = (*identityRes)(nil)
|
||||
_ mainflux.Response = (*removeRes)(nil)
|
||||
_ mainflux.Response = (*clientRes)(nil)
|
||||
_ mainflux.Response = (*viewClientRes)(nil)
|
||||
_ mainflux.Response = (*listClientsRes)(nil)
|
||||
_ mainflux.Response = (*channelRes)(nil)
|
||||
_ mainflux.Response = (*viewChannelRes)(nil)
|
||||
_ mainflux.Response = (*listChannelsRes)(nil)
|
||||
_ mainflux.Response = (*connectionRes)(nil)
|
||||
_ mainflux.Response = (*disconnectionRes)(nil)
|
||||
)
|
||||
|
||||
type identityRes struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (res identityRes) Headers() map[string]string {
|
||||
return map[string]string{
|
||||
"X-client-id": res.id,
|
||||
}
|
||||
}
|
||||
|
||||
func (res identityRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res identityRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type removeRes struct{}
|
||||
|
||||
func (res removeRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res removeRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res removeRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type clientRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res clientRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res clientRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprint("/clients/", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res clientRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type viewClientRes struct {
|
||||
clients.Client
|
||||
}
|
||||
|
||||
func (res viewClientRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewClientRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewClientRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listClientsRes struct {
|
||||
Clients []clients.Client `json:"clients"`
|
||||
}
|
||||
|
||||
func (res listClientsRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listClientsRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listClientsRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res channelRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res channelRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprint("/channels/", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res channelRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type viewChannelRes struct {
|
||||
clients.Channel
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listChannelsRes struct {
|
||||
Channels []clients.Channel `json:"channels"`
|
||||
}
|
||||
|
||||
func (res listChannelsRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listChannelsRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listChannelsRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type connectionRes struct{}
|
||||
|
||||
func (res connectionRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res connectionRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res connectionRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type disconnectionRes struct{}
|
||||
|
||||
func (res disconnectionRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res disconnectionRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res disconnectionRes) Empty() bool {
|
||||
return true
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package api
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -12,7 +12,7 @@ import (
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
@ -24,27 +24,13 @@ var (
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc manager.Service) http.Handler {
|
||||
func MakeHandler(svc clients.Service) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
}
|
||||
|
||||
r := bone.New()
|
||||
|
||||
r.Post("/users", kithttp.NewServer(
|
||||
registrationEndpoint(svc),
|
||||
decodeCredentials,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Post("/tokens", kithttp.NewServer(
|
||||
loginEndpoint(svc),
|
||||
decodeCredentials,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Post("/clients", kithttp.NewServer(
|
||||
addClientEndpoint(svc),
|
||||
decodeClientCreation,
|
||||
@ -129,53 +115,18 @@ func MakeHandler(svc manager.Service) http.Handler {
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/access-grant", kithttp.NewServer(
|
||||
identityEndpoint(svc),
|
||||
decodeIdentity,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/channels/:id/access-grant", kithttp.NewServer(
|
||||
canAccessEndpoint(svc),
|
||||
decodeView,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.GetFunc("/version", mainflux.Version("manager"))
|
||||
r.GetFunc("/version", mainflux.Version("clients"))
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func decodeIdentity(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := identityReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeCredentials(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var user manager.User
|
||||
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userReq{user}, nil
|
||||
}
|
||||
|
||||
func decodeClientCreation(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var client manager.Client
|
||||
var client clients.Client
|
||||
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -193,7 +144,7 @@ func decodeClientUpdate(_ context.Context, r *http.Request) (interface{}, error)
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var client manager.Client
|
||||
var client clients.Client
|
||||
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -212,7 +163,7 @@ func decodeChannelCreation(_ context.Context, r *http.Request) (interface{}, err
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var channel manager.Channel
|
||||
var channel clients.Channel
|
||||
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -230,7 +181,7 @@ func decodeChannelUpdate(_ context.Context, r *http.Request) (interface{}, error
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var channel manager.Channel
|
||||
var channel clients.Channel
|
||||
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -292,8 +243,8 @@ func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeConnection(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := connectionReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
chanId: bone.GetValue(r, "chanId"),
|
||||
clientId: bone.GetValue(r, "clientId"),
|
||||
chanID: bone.GetValue(r, "chanId"),
|
||||
clientID: bone.GetValue(r, "clientId"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@ -302,14 +253,14 @@ func decodeConnection(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if ar, ok := response.(apiRes); ok {
|
||||
for k, v := range ar.headers() {
|
||||
if ar, ok := response.(mainflux.Response); ok {
|
||||
for k, v := range ar.Headers() {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
w.WriteHeader(ar.code())
|
||||
w.WriteHeader(ar.Code())
|
||||
|
||||
if ar.empty() {
|
||||
if ar.Empty() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -321,13 +272,13 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
switch err {
|
||||
case manager.ErrMalformedEntity:
|
||||
case clients.ErrMalformedEntity:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case manager.ErrUnauthorizedAccess:
|
||||
case clients.ErrUnauthorizedAccess:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case manager.ErrNotFound:
|
||||
case clients.ErrNotFound:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case manager.ErrConflict:
|
||||
case clients.ErrConflict:
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errUnsupportedContentType:
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
@ -6,50 +6,23 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
var _ manager.Service = (*loggingMiddleware)(nil)
|
||||
var _ clients.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc manager.Service
|
||||
svc clients.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the core service.
|
||||
func LoggingMiddleware(svc manager.Service, logger log.Logger) manager.Service {
|
||||
func LoggingMiddleware(svc clients.Service, logger log.Logger) clients.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Register(user manager.User) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method register for user %s took %s to complete", user.Email, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Register(user)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Login(user manager.User) (token string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method login for user %s took %s to complete", user.Email, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Login(user)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) AddClient(key string, client manager.Client) (id string, err error) {
|
||||
func (lm *loggingMiddleware) AddClient(key string, client clients.Client) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method add_client for key %s and client %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
@ -62,7 +35,7 @@ func (lm *loggingMiddleware) AddClient(key string, client manager.Client) (id st
|
||||
return lm.svc.AddClient(key, client)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateClient(key string, client manager.Client) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateClient(key string, client clients.Client) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_client for key %s and client %s took %s to complete", key, client.ID, time.Since(begin))
|
||||
if err != nil {
|
||||
@ -75,7 +48,7 @@ func (lm *loggingMiddleware) UpdateClient(key string, client manager.Client) (er
|
||||
return lm.svc.UpdateClient(key, client)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewClient(key string, id string) (client manager.Client, err error) {
|
||||
func (lm *loggingMiddleware) ViewClient(key string, id string) (client clients.Client, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view_client for key %s and client %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
@ -88,7 +61,7 @@ func (lm *loggingMiddleware) ViewClient(key string, id string) (client manager.C
|
||||
return lm.svc.ViewClient(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListClients(key string, offset, limit int) (clients []manager.Client, err error) {
|
||||
func (lm *loggingMiddleware) ListClients(key string, offset, limit int) (clients []clients.Client, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_clients for key %s took %s to complete", key, time.Since(begin))
|
||||
if err != nil {
|
||||
@ -114,7 +87,7 @@ func (lm *loggingMiddleware) RemoveClient(key string, id string) (err error) {
|
||||
return lm.svc.RemoveClient(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateChannel(key string, channel manager.Channel) (id string, err error) {
|
||||
func (lm *loggingMiddleware) CreateChannel(key string, channel clients.Channel) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method create_channel for key %s and channel %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
@ -127,7 +100,7 @@ func (lm *loggingMiddleware) CreateChannel(key string, channel manager.Channel)
|
||||
return lm.svc.CreateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannel(key string, channel manager.Channel) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateChannel(key string, channel clients.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_channel for key %s and channel %s took %s to complete", key, channel.ID, time.Since(begin))
|
||||
if err != nil {
|
||||
@ -140,7 +113,7 @@ func (lm *loggingMiddleware) UpdateChannel(key string, channel manager.Channel)
|
||||
return lm.svc.UpdateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewChannel(key string, id string) (channel manager.Channel, err error) {
|
||||
func (lm *loggingMiddleware) ViewChannel(key string, id string) (channel clients.Channel, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view_channel for key %s and channel %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
@ -153,7 +126,7 @@ func (lm *loggingMiddleware) ViewChannel(key string, id string) (channel manager
|
||||
return lm.svc.ViewChannel(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListChannels(key string, offset, limit int) (channels []manager.Channel, err error) {
|
||||
func (lm *loggingMiddleware) ListChannels(key string, offset, limit int) (channels []clients.Channel, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_channels for key %s took %s to complete", key, time.Since(begin))
|
||||
if err != nil {
|
||||
@ -205,19 +178,6 @@ func (lm *loggingMiddleware) Disconnect(key, chanID, clientID string) (err error
|
||||
return lm.svc.Disconnect(key, chanID, clientID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Identity(key string) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method identity for client %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Identity(key)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CanAccess(key string, id string) (pub string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method can_access for key %s, channel %s and publisher %s took %s to complete", key, id, pub, time.Since(begin))
|
@ -6,20 +6,20 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var _ manager.Service = (*metricsMiddleware)(nil)
|
||||
var _ clients.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc manager.Service
|
||||
svc clients.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and
|
||||
// latency.
|
||||
func MetricsMiddleware(svc manager.Service, counter metrics.Counter, latency metrics.Histogram) manager.Service {
|
||||
func MetricsMiddleware(svc clients.Service, counter metrics.Counter, latency metrics.Histogram) clients.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
@ -27,25 +27,7 @@ func MetricsMiddleware(svc manager.Service, counter metrics.Counter, latency met
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Register(user manager.User) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "register").Add(1)
|
||||
ms.latency.With("method", "register").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Register(user)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Login(user manager.User) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "login").Add(1)
|
||||
ms.latency.With("method", "login").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Login(user)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) AddClient(key string, client manager.Client) (string, error) {
|
||||
func (ms *metricsMiddleware) AddClient(key string, client clients.Client) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "add_client").Add(1)
|
||||
ms.latency.With("method", "add_client").Observe(time.Since(begin).Seconds())
|
||||
@ -54,7 +36,7 @@ func (ms *metricsMiddleware) AddClient(key string, client manager.Client) (strin
|
||||
return ms.svc.AddClient(key, client)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) UpdateClient(key string, client manager.Client) error {
|
||||
func (ms *metricsMiddleware) UpdateClient(key string, client clients.Client) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "update_client").Add(1)
|
||||
ms.latency.With("method", "update_client").Observe(time.Since(begin).Seconds())
|
||||
@ -63,7 +45,7 @@ func (ms *metricsMiddleware) UpdateClient(key string, client manager.Client) err
|
||||
return ms.svc.UpdateClient(key, client)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ViewClient(key string, id string) (manager.Client, error) {
|
||||
func (ms *metricsMiddleware) ViewClient(key string, id string) (clients.Client, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_client").Add(1)
|
||||
ms.latency.With("method", "view_client").Observe(time.Since(begin).Seconds())
|
||||
@ -72,7 +54,7 @@ func (ms *metricsMiddleware) ViewClient(key string, id string) (manager.Client,
|
||||
return ms.svc.ViewClient(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListClients(key string, offset, limit int) ([]manager.Client, error) {
|
||||
func (ms *metricsMiddleware) ListClients(key string, offset, limit int) ([]clients.Client, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_clients").Add(1)
|
||||
ms.latency.With("method", "list_clients").Observe(time.Since(begin).Seconds())
|
||||
@ -90,7 +72,7 @@ func (ms *metricsMiddleware) RemoveClient(key string, id string) error {
|
||||
return ms.svc.RemoveClient(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CreateChannel(key string, channel manager.Channel) (string, error) {
|
||||
func (ms *metricsMiddleware) CreateChannel(key string, channel clients.Channel) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "create_channel").Add(1)
|
||||
ms.latency.With("method", "create_channel").Observe(time.Since(begin).Seconds())
|
||||
@ -99,7 +81,7 @@ func (ms *metricsMiddleware) CreateChannel(key string, channel manager.Channel)
|
||||
return ms.svc.CreateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) UpdateChannel(key string, channel manager.Channel) error {
|
||||
func (ms *metricsMiddleware) UpdateChannel(key string, channel clients.Channel) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "update_channel").Add(1)
|
||||
ms.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
|
||||
@ -108,7 +90,7 @@ func (ms *metricsMiddleware) UpdateChannel(key string, channel manager.Channel)
|
||||
return ms.svc.UpdateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ViewChannel(key string, id string) (manager.Channel, error) {
|
||||
func (ms *metricsMiddleware) ViewChannel(key string, id string) (clients.Channel, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_channel").Add(1)
|
||||
ms.latency.With("method", "view_channel").Observe(time.Since(begin).Seconds())
|
||||
@ -117,7 +99,7 @@ func (ms *metricsMiddleware) ViewChannel(key string, id string) (manager.Channel
|
||||
return ms.svc.ViewChannel(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListChannels(key string, offset, limit int) ([]manager.Channel, error) {
|
||||
func (ms *metricsMiddleware) ListChannels(key string, offset, limit int) ([]clients.Channel, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_channels").Add(1)
|
||||
ms.latency.With("method", "list_channels").Observe(time.Since(begin).Seconds())
|
||||
@ -135,31 +117,22 @@ func (ms *metricsMiddleware) RemoveChannel(key string, id string) error {
|
||||
return ms.svc.RemoveChannel(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Connect(key, chanId, clientId string) error {
|
||||
func (ms *metricsMiddleware) Connect(key, chanID, clientID string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "connect").Add(1)
|
||||
ms.latency.With("method", "connect").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Connect(key, chanId, clientId)
|
||||
return ms.svc.Connect(key, chanID, clientID)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Disconnect(key, chanId, clientId string) error {
|
||||
func (ms *metricsMiddleware) Disconnect(key, chanID, clientID string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "disconnect").Add(1)
|
||||
ms.latency.With("method", "disconnect").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Disconnect(key, chanId, clientId)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Identity(key string) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "identity").Add(1)
|
||||
ms.latency.With("method", "identity").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Identity(key)
|
||||
return ms.svc.Disconnect(key, chanID, clientID)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CanAccess(key string, id string) (string, error) {
|
31
clients/bcrypt/hasher.go
Normal file
31
clients/bcrypt/hasher.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Package bcrypt provides a hasher implementation utilising bcrypt.
|
||||
package bcrypt
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const cost int = 10
|
||||
|
||||
var _ clients.Hasher = (*bcryptHasher)(nil)
|
||||
|
||||
type bcryptHasher struct{}
|
||||
|
||||
// New instantiates a bcrypt-based hasher implementation.
|
||||
func New() clients.Hasher {
|
||||
return &bcryptHasher{}
|
||||
}
|
||||
|
||||
func (bh *bcryptHasher) Hash(pwd string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), cost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
func (bh *bcryptHasher) Compare(plain, hashed string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain))
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package manager
|
||||
package clients
|
||||
|
||||
// Channel represents a Mainflux "communication group". This group contains the
|
||||
// clients that can exchange messages between eachother.
|
@ -1,4 +1,4 @@
|
||||
package manager
|
||||
package clients
|
||||
|
||||
import "strings"
|
||||
|
||||
@ -13,7 +13,7 @@ type Client struct {
|
||||
Payload string `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
var clientTypes map[string]bool = map[string]bool{
|
||||
var clientTypes = map[string]bool{
|
||||
"app": true,
|
||||
"device": true,
|
||||
}
|
||||
@ -29,8 +29,8 @@ func (c *Client) Validate() error {
|
||||
|
||||
// ClientRepository specifies a client persistence API.
|
||||
type ClientRepository interface {
|
||||
// Id generates new resource identifier.
|
||||
Id() string
|
||||
// ID generates new resource identifier.
|
||||
ID() string
|
||||
|
||||
// Save persists the client. Successful operation is indicated by non-nil
|
||||
// error response.
|
194
clients/clients.go
Normal file
194
clients/clients.go
Normal file
@ -0,0 +1,194 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
var _ Service = (*clientsService)(nil)
|
||||
|
||||
type clientsService struct {
|
||||
users mainflux.UsersServiceClient
|
||||
clients ClientRepository
|
||||
channels ChannelRepository
|
||||
hasher Hasher
|
||||
idp IdentityProvider
|
||||
}
|
||||
|
||||
// New instantiates the domain service implementation.
|
||||
func New(users mainflux.UsersServiceClient, clients ClientRepository, channels ChannelRepository, hasher Hasher, idp IdentityProvider) Service {
|
||||
return &clientsService{
|
||||
users: users,
|
||||
clients: clients,
|
||||
channels: channels,
|
||||
hasher: hasher,
|
||||
idp: idp,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *clientsService) AddClient(key string, client Client) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
client.ID = ms.clients.ID()
|
||||
client.Owner = res.GetValue()
|
||||
client.Key, _ = ms.idp.PermanentKey(client.ID)
|
||||
|
||||
return client.ID, ms.clients.Save(client)
|
||||
}
|
||||
|
||||
func (ms *clientsService) UpdateClient(key string, client Client) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
client.Owner = res.GetValue()
|
||||
|
||||
return ms.clients.Update(client)
|
||||
}
|
||||
|
||||
func (ms *clientsService) ViewClient(key, id string) (Client, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return Client{}, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.clients.One(res.GetValue(), id)
|
||||
}
|
||||
|
||||
func (ms *clientsService) ListClients(key string, offset, limit int) ([]Client, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return nil, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.clients.All(res.GetValue(), offset, limit), nil
|
||||
}
|
||||
|
||||
func (ms *clientsService) RemoveClient(key, id string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.clients.Remove(res.GetValue(), id)
|
||||
}
|
||||
|
||||
func (ms *clientsService) CreateChannel(key string, channel Channel) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
channel.Owner = res.GetValue()
|
||||
return ms.channels.Save(channel)
|
||||
}
|
||||
|
||||
func (ms *clientsService) UpdateChannel(key string, channel Channel) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
channel.Owner = res.GetValue()
|
||||
return ms.channels.Update(channel)
|
||||
}
|
||||
|
||||
func (ms *clientsService) ViewChannel(key, id string) (Channel, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return Channel{}, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.One(res.GetValue(), id)
|
||||
}
|
||||
|
||||
func (ms *clientsService) ListChannels(key string, offset, limit int) ([]Channel, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return nil, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.All(res.GetValue(), offset, limit), nil
|
||||
}
|
||||
|
||||
func (ms *clientsService) RemoveChannel(key, id string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.Remove(res.GetValue(), id)
|
||||
}
|
||||
|
||||
func (ms *clientsService) Connect(key, chanID, clientID string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.Connect(res.GetValue(), chanID, clientID)
|
||||
}
|
||||
|
||||
func (ms *clientsService) Disconnect(key, chanID, clientID string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.Disconnect(res.GetValue(), chanID, clientID)
|
||||
}
|
||||
|
||||
func (ms *clientsService) CanAccess(key, channel string) (string, error) {
|
||||
client, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !ms.channels.HasClient(channel, client) {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
342
clients/clients_test.go
Normal file
342
clients/clients_test.go
Normal file
@ -0,0 +1,342 @@
|
||||
package clients_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/mainflux/mainflux/clients/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
wrong = "wrong-value"
|
||||
email = "user@example.com"
|
||||
token = "token"
|
||||
)
|
||||
|
||||
var (
|
||||
client = clients.Client{Type: "app", Name: "test"}
|
||||
channel = clients.Channel{Name: "test", Clients: []clients.Client{}}
|
||||
)
|
||||
|
||||
func newService(tokens map[string]string) clients.Service {
|
||||
users := mocks.NewUsersService(tokens)
|
||||
clientsRepo := mocks.NewClientRepository()
|
||||
channelsRepo := mocks.NewChannelRepository(clientsRepo)
|
||||
hasher := mocks.NewHasher()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
|
||||
return clients.New(users, clientsRepo, channelsRepo, hasher, idp)
|
||||
}
|
||||
|
||||
func TestAddClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
|
||||
cases := map[string]struct {
|
||||
client clients.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"add new app": {clients.Client{Type: "app", Name: "a"}, token, nil},
|
||||
"add new device": {clients.Client{Type: "device", Name: "b"}, token, nil},
|
||||
"add client with wrong credentials": {clients.Client{Type: "app", Name: "d"}, wrong, clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.AddClient(tc.key, tc.client)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
client.ID = clientID
|
||||
|
||||
cases := map[string]struct {
|
||||
client clients.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"update existing client": {client, token, nil},
|
||||
"update client with wrong credentials": {client, wrong, clients.ErrUnauthorizedAccess},
|
||||
"update non-existing client": {clients.Client{ID: "2", Type: "app", Name: "d"}, token, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.UpdateClient(tc.key, tc.client)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
client.ID = clientID
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"view existing client": {client.ID, token, nil},
|
||||
"view client with wrong credentials": {client.ID, wrong, clients.ErrUnauthorizedAccess},
|
||||
"view non-existing client": {wrong, token, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ViewClient(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListClients(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
|
||||
n := 10
|
||||
for i := 0; i < n; i++ {
|
||||
svc.AddClient(token, client)
|
||||
}
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
offset int
|
||||
limit int
|
||||
size int
|
||||
err error
|
||||
}{
|
||||
"list clients": {token, 0, 5, 5, nil},
|
||||
"list clients 5-10": {token, 5, 10, 5, nil},
|
||||
"list last client": {token, 9, 10, 1, nil},
|
||||
"list empty response": {token, 11, 10, 0, nil},
|
||||
"list offset < 0": {token, -1, 10, 0, nil},
|
||||
"list limit < 0": {token, 1, -10, 0, nil},
|
||||
"list limit = 0": {token, 1, 0, 0, nil},
|
||||
"list clients with wrong credentials": {wrong, 0, 0, 0, clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
cl, err := svc.ListClients(tc.key, tc.offset, tc.limit)
|
||||
size := len(cl)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
client.ID = clientID
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"remove client with wrong credentials": {client.ID, "?", clients.ErrUnauthorizedAccess},
|
||||
"remove existing client": {client.ID, token, nil},
|
||||
"remove removed client": {client.ID, token, nil},
|
||||
"remove non-existing client": {"?", token, nil},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.RemoveClient(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
|
||||
cases := map[string]struct {
|
||||
channel clients.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"create channel": {clients.Channel{}, token, nil},
|
||||
"create channel with wrong credentials": {clients.Channel{}, wrong, clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.CreateChannel(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = chanID
|
||||
|
||||
cases := map[string]struct {
|
||||
channel clients.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"update existing channel": {channel, token, nil},
|
||||
"update channel with wrong credentials": {channel, wrong, clients.ErrUnauthorizedAccess},
|
||||
"update non-existing channel": {clients.Channel{ID: "2", Name: "test"}, token, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.UpdateChannel(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = chanID
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"view existing channel": {channel.ID, token, nil},
|
||||
"view channel with wrong credentials": {channel.ID, wrong, clients.ErrUnauthorizedAccess},
|
||||
"view non-existing channel": {wrong, token, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ViewChannel(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListChannels(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
|
||||
n := 10
|
||||
for i := 0; i < n; i++ {
|
||||
svc.CreateChannel(token, channel)
|
||||
}
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
offset int
|
||||
limit int
|
||||
size int
|
||||
err error
|
||||
}{
|
||||
"list first 5 channels": {token, 0, 5, 5, nil},
|
||||
"list channels 5-10 channels": {token, 5, 10, 5, nil},
|
||||
"list last channel": {token, 6, 10, 4, nil},
|
||||
"list offset < 0": {token, -1, 10, 0, nil},
|
||||
"list limit < 0": {token, 1, -10, 0, nil},
|
||||
"list limit = 0": {token, 1, 0, 0, nil},
|
||||
"list channels with wrong credentials": {wrong, 0, 0, 0, clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
ch, err := svc.ListChannels(tc.key, tc.offset, tc.limit)
|
||||
size := len(ch)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = chanID
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"remove channel with wrong credentials": {channel.ID, wrong, clients.ErrUnauthorizedAccess},
|
||||
"remove existing channel": {channel.ID, token, nil},
|
||||
"remove removed channel": {channel.ID, token, nil},
|
||||
"remove non-existing channel": {channel.ID, token, nil},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.RemoveChannel(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
client.ID = clientID
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = chanID
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
chanID string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
"connect client": {token, channel.ID, client.ID, nil},
|
||||
"connect client with wrong credentials": {wrong, channel.ID, client.ID, clients.ErrUnauthorizedAccess},
|
||||
"connect client to non-existing channel": {token, wrong, client.ID, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.Connect(tc.key, tc.chanID, tc.clientID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnect(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
client.ID = clientID
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = chanID
|
||||
|
||||
svc.Connect(token, chanID, clientID)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
chanID string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
{"disconnect connected client", token, channel.ID, client.ID, nil},
|
||||
{"disconnect disconnected client", token, channel.ID, client.ID, clients.ErrNotFound},
|
||||
{"disconnect client with wrong credentials", wrong, channel.ID, client.ID, clients.ErrUnauthorizedAccess},
|
||||
{"disconnect client from non-existing channel", token, wrong, client.ID, clients.ErrNotFound},
|
||||
{"disconnect non-existing client", token, channel.ID, wrong, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Disconnect(tc.key, tc.chanID, tc.clientID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCanAccess(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
client.ID = clientID
|
||||
client.Key = clientID
|
||||
|
||||
channel.Clients = []clients.Client{client}
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = chanID
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
channel string
|
||||
err error
|
||||
}{
|
||||
"allowed access": {client.Key, channel.ID, nil},
|
||||
"not-connected cannot access": {wrong, channel.ID, clients.ErrUnauthorizedAccess},
|
||||
"access non-existing channel": {client.Key, wrong, clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.CanAccess(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
3
clients/doc.go
Normal file
3
clients/doc.go
Normal file
@ -0,0 +1,3 @@
|
||||
// Package clients contains the domain concept definitions needed to support
|
||||
// Mainflux clients service functionality.
|
||||
package clients
|
@ -1,10 +1,11 @@
|
||||
// Package jwt provides a JWT identity provider.
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -12,14 +13,14 @@ const (
|
||||
duration time.Duration = 10 * time.Hour
|
||||
)
|
||||
|
||||
var _ manager.IdentityProvider = (*jwtIdentityProvider)(nil)
|
||||
var _ clients.IdentityProvider = (*jwtIdentityProvider)(nil)
|
||||
|
||||
type jwtIdentityProvider struct {
|
||||
secret string
|
||||
}
|
||||
|
||||
// New instantiates a JWT identity provider.
|
||||
func New(secret string) manager.IdentityProvider {
|
||||
func New(secret string) clients.IdentityProvider {
|
||||
return &jwtIdentityProvider{secret}
|
||||
}
|
||||
|
||||
@ -55,19 +56,19 @@ func (idp *jwtIdentityProvider) jwt(claims jwt.StandardClaims) (string, error) {
|
||||
func (idp *jwtIdentityProvider) Identity(key string) (string, error) {
|
||||
token, err := jwt.Parse(key, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, manager.ErrUnauthorizedAccess
|
||||
return nil, clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return []byte(idp.secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", manager.ErrUnauthorizedAccess
|
||||
return "", clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return claims["sub"].(string), nil
|
||||
}
|
||||
|
||||
return "", manager.ErrUnauthorizedAccess
|
||||
return "", clients.ErrUnauthorizedAccess
|
||||
}
|
@ -5,75 +5,75 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var _ manager.ChannelRepository = (*channelRepositoryMock)(nil)
|
||||
var _ clients.ChannelRepository = (*channelRepositoryMock)(nil)
|
||||
|
||||
const chanId = "123e4567-e89b-12d3-a456-"
|
||||
const chanID = "123e4567-e89b-12d3-a456-"
|
||||
|
||||
type channelRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
channels map[string]manager.Channel
|
||||
clients manager.ClientRepository
|
||||
channels map[string]clients.Channel
|
||||
clients clients.ClientRepository
|
||||
}
|
||||
|
||||
// NewChannelRepository creates in-memory channel repository.
|
||||
func NewChannelRepository(clients manager.ClientRepository) manager.ChannelRepository {
|
||||
func NewChannelRepository(repo clients.ClientRepository) clients.ChannelRepository {
|
||||
return &channelRepositoryMock{
|
||||
channels: make(map[string]manager.Channel),
|
||||
clients: clients,
|
||||
channels: make(map[string]clients.Channel),
|
||||
clients: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Save(channel manager.Channel) (string, error) {
|
||||
func (crm *channelRepositoryMock) Save(channel clients.Channel) (string, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.counter += 1
|
||||
channel.ID = fmt.Sprintf("%s%012d", chanId, crm.counter)
|
||||
crm.counter++
|
||||
channel.ID = fmt.Sprintf("%s%012d", chanID, crm.counter)
|
||||
|
||||
crm.channels[key(channel.Owner, channel.ID)] = channel
|
||||
|
||||
return channel.ID, nil
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Update(channel manager.Channel) error {
|
||||
func (crm *channelRepositoryMock) Update(channel clients.Channel) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
dbKey := key(channel.Owner, channel.ID)
|
||||
|
||||
if _, ok := crm.channels[dbKey]; !ok {
|
||||
return manager.ErrNotFound
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
crm.channels[dbKey] = channel
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) One(owner, id string) (manager.Channel, error) {
|
||||
func (crm *channelRepositoryMock) One(owner, id string) (clients.Channel, error) {
|
||||
if c, ok := crm.channels[key(owner, id)]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return manager.Channel{}, manager.ErrNotFound
|
||||
return clients.Channel{}, clients.ErrNotFound
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) All(owner string, offset, limit int) []manager.Channel {
|
||||
func (crm *channelRepositoryMock) All(owner string, offset, limit int) []clients.Channel {
|
||||
// This obscure way to examine map keys is enforced by the key structure
|
||||
// itself (see mocks/commons.go).
|
||||
prefix := fmt.Sprintf("%s-", owner)
|
||||
channels := make([]manager.Channel, 0)
|
||||
channels := make([]clients.Channel, 0)
|
||||
|
||||
if offset < 0 || limit <= 0 {
|
||||
return channels
|
||||
}
|
||||
|
||||
// Since IDs starts from 1, shift everything by one.
|
||||
first := fmt.Sprintf("%s%012d", chanId, offset+1)
|
||||
last := fmt.Sprintf("%s%012d", chanId, offset+limit+1)
|
||||
first := fmt.Sprintf("%s%012d", chanID, offset+1)
|
||||
last := fmt.Sprintf("%s%012d", chanID, offset+limit+1)
|
||||
|
||||
for k, v := range crm.channels {
|
||||
if strings.HasPrefix(k, prefix) && v.ID >= first && v.ID < last {
|
||||
@ -89,13 +89,13 @@ func (crm *channelRepositoryMock) Remove(owner, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Connect(owner, chanId, clientId string) error {
|
||||
channel, err := crm.One(owner, chanId)
|
||||
func (crm *channelRepositoryMock) Connect(owner, chanID, clientID string) error {
|
||||
channel, err := crm.One(owner, chanID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := crm.clients.One(owner, clientId)
|
||||
client, err := crm.clients.One(owner, clientID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -103,19 +103,19 @@ func (crm *channelRepositoryMock) Connect(owner, chanId, clientId string) error
|
||||
return crm.Update(channel)
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Disconnect(owner, chanId, clientId string) error {
|
||||
channel, err := crm.One(owner, chanId)
|
||||
func (crm *channelRepositoryMock) Disconnect(owner, chanID, clientID string) error {
|
||||
channel, err := crm.One(owner, chanID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !crm.HasClient(chanId, clientId) {
|
||||
return manager.ErrNotFound
|
||||
if !crm.HasClient(chanID, clientID) {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
connected := make([]manager.Client, len(channel.Clients)-1)
|
||||
connected := make([]clients.Client, len(channel.Clients)-1)
|
||||
for _, client := range channel.Clients {
|
||||
if client.ID != clientId {
|
||||
if client.ID != clientID {
|
||||
connected = append(connected, client)
|
||||
}
|
||||
}
|
@ -5,35 +5,35 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var _ manager.ClientRepository = (*clientRepositoryMock)(nil)
|
||||
var _ clients.ClientRepository = (*clientRepositoryMock)(nil)
|
||||
|
||||
const cliId = "123e4567-e89b-12d3-a456-"
|
||||
const cliID = "123e4567-e89b-12d3-a456-"
|
||||
|
||||
type clientRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
clients map[string]manager.Client
|
||||
clients map[string]clients.Client
|
||||
}
|
||||
|
||||
// NewClientRepository creates in-memory client repository.
|
||||
func NewClientRepository() manager.ClientRepository {
|
||||
func NewClientRepository() clients.ClientRepository {
|
||||
return &clientRepositoryMock{
|
||||
clients: make(map[string]manager.Client),
|
||||
clients: make(map[string]clients.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Id() string {
|
||||
func (crm *clientRepositoryMock) ID() string {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.counter += 1
|
||||
return fmt.Sprintf("%s%012d", cliId, crm.counter)
|
||||
crm.counter++
|
||||
return fmt.Sprintf("%s%012d", cliID, crm.counter)
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Save(client manager.Client) error {
|
||||
func (crm *clientRepositoryMock) Save(client clients.Client) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
@ -42,14 +42,14 @@ func (crm *clientRepositoryMock) Save(client manager.Client) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Update(client manager.Client) error {
|
||||
func (crm *clientRepositoryMock) Update(client clients.Client) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
dbKey := key(client.Owner, client.ID)
|
||||
|
||||
if _, ok := crm.clients[dbKey]; !ok {
|
||||
return manager.ErrNotFound
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
crm.clients[dbKey] = client
|
||||
@ -57,27 +57,27 @@ func (crm *clientRepositoryMock) Update(client manager.Client) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) One(owner, id string) (manager.Client, error) {
|
||||
func (crm *clientRepositoryMock) One(owner, id string) (clients.Client, error) {
|
||||
if c, ok := crm.clients[key(owner, id)]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return manager.Client{}, manager.ErrNotFound
|
||||
return clients.Client{}, clients.ErrNotFound
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) All(owner string, offset, limit int) []manager.Client {
|
||||
func (crm *clientRepositoryMock) All(owner string, offset, limit int) []clients.Client {
|
||||
// This obscure way to examine map keys is enforced by the key structure
|
||||
// itself (see mocks/commons.go).
|
||||
prefix := fmt.Sprintf("%s-", owner)
|
||||
clients := make([]manager.Client, 0)
|
||||
clients := make([]clients.Client, 0)
|
||||
|
||||
if offset < 0 || limit <= 0 {
|
||||
return clients
|
||||
}
|
||||
|
||||
// Since IDs start from 1, shift everything by one.
|
||||
first := fmt.Sprintf("%s%012d", cliId, offset+1)
|
||||
last := fmt.Sprintf("%s%012d", cliId, offset+limit+1)
|
||||
first := fmt.Sprintf("%s%012d", cliID, offset+1)
|
||||
last := fmt.Sprintf("%s%012d", cliID, offset+limit+1)
|
||||
|
||||
for k, v := range crm.clients {
|
||||
if strings.HasPrefix(k, prefix) && v.ID >= first && v.ID < last {
|
@ -1,10 +1,10 @@
|
||||
package mocks
|
||||
|
||||
import "github.com/mainflux/mainflux/manager"
|
||||
import "github.com/mainflux/mainflux/clients"
|
||||
|
||||
var (
|
||||
_ manager.Hasher = (*hasherMock)(nil)
|
||||
_ manager.IdentityProvider = (*identityProviderMock)(nil)
|
||||
_ clients.Hasher = (*hasherMock)(nil)
|
||||
_ clients.IdentityProvider = (*identityProviderMock)(nil)
|
||||
)
|
||||
|
||||
type hasherMock struct{}
|
||||
@ -15,7 +15,7 @@ func (hm *hasherMock) Hash(pwd string) (string, error) {
|
||||
|
||||
func (hm *hasherMock) Compare(plain, hashed string) error {
|
||||
if plain != hashed {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -25,7 +25,7 @@ type identityProviderMock struct{}
|
||||
|
||||
func (idp *identityProviderMock) TemporaryKey(id string) (string, error) {
|
||||
if id == "" {
|
||||
return "", manager.ErrUnauthorizedAccess
|
||||
return "", clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return id, nil
|
||||
@ -41,12 +41,12 @@ func (idp *identityProviderMock) Identity(key string) (string, error) {
|
||||
|
||||
// NewHasher creates "no-op" hasher for test purposes. This implementation will
|
||||
// return secrets without changing them.
|
||||
func NewHasher() manager.Hasher {
|
||||
func NewHasher() clients.Hasher {
|
||||
return &hasherMock{}
|
||||
}
|
||||
|
||||
// NewIdentityProvider creates "mirror" identity provider, i.e. generated
|
||||
// token will hold value provided by the caller.
|
||||
func NewIdentityProvider() manager.IdentityProvider {
|
||||
func NewIdentityProvider() clients.IdentityProvider {
|
||||
return &identityProviderMock{}
|
||||
}
|
27
clients/mocks/users.go
Normal file
27
clients/mocks/users.go
Normal file
@ -0,0 +1,27 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.UsersServiceClient = (*usersServiceMock)(nil)
|
||||
|
||||
type usersServiceMock struct {
|
||||
users map[string]string
|
||||
}
|
||||
|
||||
// NewUsersService creates mock of users service.
|
||||
func NewUsersService(users map[string]string) mainflux.UsersServiceClient {
|
||||
return &usersServiceMock{users}
|
||||
}
|
||||
|
||||
func (svc usersServiceMock) Identify(ctx context.Context, in *mainflux.Token, opts ...grpc.CallOption) (*mainflux.Identity, error) {
|
||||
if id, ok := svc.users[in.Value]; ok {
|
||||
return &mainflux.Identity{id}, nil
|
||||
}
|
||||
return nil, users.ErrUnauthorizedAccess
|
||||
}
|
@ -3,11 +3,11 @@ package postgres
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres" // required by GORM
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
var _ manager.ChannelRepository = (*channelRepository)(nil)
|
||||
var _ clients.ChannelRepository = (*channelRepository)(nil)
|
||||
|
||||
type channelRepository struct {
|
||||
db *gorm.DB
|
||||
@ -15,11 +15,11 @@ type channelRepository struct {
|
||||
|
||||
// NewChannelRepository instantiates a PostgreSQL implementation of channel
|
||||
// repository.
|
||||
func NewChannelRepository(db *gorm.DB) manager.ChannelRepository {
|
||||
func NewChannelRepository(db *gorm.DB) clients.ChannelRepository {
|
||||
return &channelRepository{db}
|
||||
}
|
||||
|
||||
func (cr channelRepository) Save(channel manager.Channel) (string, error) {
|
||||
func (cr channelRepository) Save(channel clients.Channel) (string, error) {
|
||||
channel.ID = uuid.NewV4().String()
|
||||
|
||||
if err := cr.db.Create(&channel).Error; err != nil {
|
||||
@ -29,25 +29,25 @@ func (cr channelRepository) Save(channel manager.Channel) (string, error) {
|
||||
return channel.ID, nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) Update(channel manager.Channel) error {
|
||||
func (cr channelRepository) Update(channel clients.Channel) error {
|
||||
sql := "UPDATE channels SET name = ? WHERE owner = ? AND id = ?;"
|
||||
res := cr.db.Exec(sql, channel.Name, channel.Owner, channel.ID)
|
||||
|
||||
if res.Error == nil && res.RowsAffected == 0 {
|
||||
return manager.ErrNotFound
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return res.Error
|
||||
}
|
||||
|
||||
func (cr channelRepository) One(owner, id string) (manager.Channel, error) {
|
||||
channel := manager.Channel{}
|
||||
func (cr channelRepository) One(owner, id string) (clients.Channel, error) {
|
||||
channel := clients.Channel{}
|
||||
|
||||
res := cr.db.Preload("Clients").First(&channel, "owner = ? AND id = ?", owner, id)
|
||||
|
||||
if err := res.Error; err != nil {
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
return channel, manager.ErrNotFound
|
||||
return channel, clients.ErrNotFound
|
||||
}
|
||||
|
||||
return channel, err
|
||||
@ -56,19 +56,19 @@ func (cr channelRepository) One(owner, id string) (manager.Channel, error) {
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) All(owner string, offset, limit int) []manager.Channel {
|
||||
var channels []manager.Channel
|
||||
func (cr channelRepository) All(owner string, offset, limit int) []clients.Channel {
|
||||
var channels []clients.Channel
|
||||
|
||||
cr.db.Offset(offset).Limit(limit).Find(&channels, "owner = ?", owner)
|
||||
return channels
|
||||
}
|
||||
|
||||
func (cr channelRepository) Remove(owner, id string) error {
|
||||
cr.db.Delete(&manager.Channel{}, "owner = ? AND id = ?", owner, id)
|
||||
cr.db.Delete(&clients.Channel{}, "owner = ? AND id = ?", owner, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) Connect(owner, chanId, clientId string) error {
|
||||
func (cr channelRepository) Connect(owner, chanID, clientID string) error {
|
||||
// This approach can be replaced by declaring composite keys on both tables
|
||||
// (clients and channels), and then propagate them into the m2m table. For
|
||||
// some reason GORM does not infer these kind of connections well and
|
||||
@ -80,35 +80,35 @@ func (cr channelRepository) Connect(owner, chanId, clientId string) error {
|
||||
EXISTS (SELECT 1 FROM channels WHERE owner = ? AND id = ?) AND
|
||||
EXISTS (SELECT 1 FROM clients WHERE owner = ? AND id = ?);`
|
||||
|
||||
res := cr.db.Exec(sql, chanId, clientId, owner, chanId, owner, clientId)
|
||||
res := cr.db.Exec(sql, chanID, clientID, owner, chanID, owner, clientID)
|
||||
|
||||
if res.Error == nil && res.RowsAffected == 0 {
|
||||
return manager.ErrNotFound
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return res.Error
|
||||
}
|
||||
|
||||
func (cr channelRepository) Disconnect(owner, chanId, clientId string) error {
|
||||
func (cr channelRepository) Disconnect(owner, chanID, clientID string) error {
|
||||
// The same remark given in Connect applies here.
|
||||
sql := `DELETE FROM channel_clients WHERE
|
||||
channel_id = ? AND client_id = ? AND
|
||||
EXISTS (SELECT 1 FROM channels WHERE owner = ? AND id = ?) AND
|
||||
EXISTS (SELECT 1 FROM clients WHERE owner = ? AND id = ?);`
|
||||
|
||||
res := cr.db.Exec(sql, chanId, clientId, owner, chanId, owner, clientId)
|
||||
res := cr.db.Exec(sql, chanID, clientID, owner, chanID, owner, clientID)
|
||||
|
||||
if res.Error == nil && res.RowsAffected == 0 {
|
||||
return manager.ErrNotFound
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return res.Error
|
||||
}
|
||||
|
||||
func (cr channelRepository) HasClient(chanId, clientId string) bool {
|
||||
func (cr channelRepository) HasClient(chanID, clientID string) bool {
|
||||
sql := "SELECT EXISTS (SELECT 1 FROM channel_clients WHERE channel_id = $1 AND client_id = $2);"
|
||||
|
||||
row := cr.db.DB().QueryRow(sql, chanId, clientId)
|
||||
row := cr.db.DB().QueryRow(sql, chanID, clientID)
|
||||
|
||||
var exists bool
|
||||
if err := row.Scan(&exists); err != nil {
|
@ -4,56 +4,38 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/mainflux/mainflux/clients/postgres"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChannelSave(t *testing.T) {
|
||||
email := "channel-save@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
c1 := manager.Channel{Owner: email}
|
||||
c2 := manager.Channel{Owner: wrong}
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
hasErr bool
|
||||
}{
|
||||
"new channel, existing user": {c1, false},
|
||||
"new channel, non-existing user": {c2, true},
|
||||
}
|
||||
channel := clients.Channel{Owner: email}
|
||||
|
||||
channelRepo := postgres.NewChannelRepository(db)
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := channelRepo.Save(tc.channel)
|
||||
hasErr := err != nil
|
||||
assert.Equal(t, tc.hasErr, hasErr, fmt.Sprintf("%s: expected %t got %t", desc, tc.hasErr, hasErr))
|
||||
}
|
||||
_, err := channelRepo.Save(channel)
|
||||
hasErr := err != nil
|
||||
assert.False(t, hasErr, fmt.Sprintf("create new channel: expected false got %t", hasErr))
|
||||
}
|
||||
|
||||
func TestChannelUpdate(t *testing.T) {
|
||||
email := "channel-update@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
|
||||
c := manager.Channel{Owner: email}
|
||||
c := clients.Channel{Owner: email}
|
||||
id, _ := chanRepo.Save(c)
|
||||
c.ID = id
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
channel clients.Channel
|
||||
err error
|
||||
}{
|
||||
"existing channel": {c, nil},
|
||||
"non-existing channel with existing user": {manager.Channel{ID: wrong, Owner: email}, manager.ErrNotFound},
|
||||
"non-existing channel with non-existing user": {manager.Channel{ID: wrong, Owner: wrong}, manager.ErrNotFound},
|
||||
"non-existing channel with existing user": {clients.Channel{ID: wrong, Owner: email}, clients.ErrNotFound},
|
||||
"non-existing channel with non-existing user": {clients.Channel{ID: wrong, Owner: wrong}, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@ -65,12 +47,9 @@ func TestChannelUpdate(t *testing.T) {
|
||||
func TestSingleChannelRetrieval(t *testing.T) {
|
||||
email := "channel-single-retrieval@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
|
||||
c := manager.Channel{Owner: email}
|
||||
c := clients.Channel{Owner: email}
|
||||
id, _ := chanRepo.Save(c)
|
||||
|
||||
cases := map[string]struct {
|
||||
@ -79,8 +58,8 @@ func TestSingleChannelRetrieval(t *testing.T) {
|
||||
err error
|
||||
}{
|
||||
"existing user": {c.Owner, id, nil},
|
||||
"existing user, non-existing channel": {c.Owner, wrong, manager.ErrNotFound},
|
||||
"non-existing owner": {wrong, id, manager.ErrNotFound},
|
||||
"existing user, non-existing channel": {c.Owner, wrong, clients.ErrNotFound},
|
||||
"non-existing owner": {wrong, id, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@ -92,15 +71,12 @@ func TestSingleChannelRetrieval(t *testing.T) {
|
||||
func TestMultiChannelRetrieval(t *testing.T) {
|
||||
email := "channel-multi-retrieval@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
|
||||
n := 10
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
c := manager.Channel{Owner: email}
|
||||
c := clients.Channel{Owner: email}
|
||||
chanRepo.Save(c)
|
||||
}
|
||||
|
||||
@ -123,21 +99,18 @@ func TestMultiChannelRetrieval(t *testing.T) {
|
||||
func TestChannelRemoval(t *testing.T) {
|
||||
email := "channel-removal@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
|
||||
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
|
||||
|
||||
// show that the removal works the same for both existing and non-existing
|
||||
// (removed) channel
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := chanRepo.Remove(email, chanId); err != nil {
|
||||
if err := chanRepo.Remove(email, chanID); err != nil {
|
||||
t.Fatalf("#%d: failed to remove channel due to: %s", i, err)
|
||||
}
|
||||
|
||||
if _, err := chanRepo.One(email, chanId); err != manager.ErrNotFound {
|
||||
t.Fatalf("#%d: expected %s got %s", i, manager.ErrNotFound, err)
|
||||
if _, err := chanRepo.One(email, chanID); err != clients.ErrNotFound {
|
||||
t.Fatalf("#%d: expected %s got %s", i, clients.ErrNotFound, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -145,33 +118,30 @@ func TestChannelRemoval(t *testing.T) {
|
||||
func TestChannelConnect(t *testing.T) {
|
||||
email := "channel-connect@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
client := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
|
||||
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
chanId string
|
||||
clientId string
|
||||
chanID string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
"existing user, channel and client": {email, chanId, client.ID, nil},
|
||||
"with non-existing user": {wrong, chanId, client.ID, manager.ErrNotFound},
|
||||
"non-existing channel": {email, wrong, client.ID, manager.ErrNotFound},
|
||||
"non-existing client": {email, chanId, wrong, manager.ErrNotFound},
|
||||
"existing user, channel and client": {email, chanID, client.ID, nil},
|
||||
"with non-existing user": {wrong, chanID, client.ID, clients.ErrNotFound},
|
||||
"non-existing channel": {email, wrong, client.ID, clients.ErrNotFound},
|
||||
"non-existing client": {email, chanID, wrong, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := chanRepo.Connect(tc.owner, tc.chanId, tc.clientId)
|
||||
err := chanRepo.Connect(tc.owner, tc.chanID, tc.clientID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@ -179,37 +149,34 @@ func TestChannelConnect(t *testing.T) {
|
||||
func TestChannelDisconnect(t *testing.T) {
|
||||
email := "channel-disconnect@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
client := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
|
||||
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
|
||||
|
||||
chanRepo.Connect(email, chanId, client.ID)
|
||||
chanRepo.Connect(email, chanID, client.ID)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
owner string
|
||||
chanId string
|
||||
clientId string
|
||||
chanID string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
{"connected client", email, chanId, client.ID, nil},
|
||||
{"non-connected client", email, chanId, client.ID, manager.ErrNotFound},
|
||||
{"non-existing user", wrong, chanId, client.ID, manager.ErrNotFound},
|
||||
{"non-existing channel", email, wrong, client.ID, manager.ErrNotFound},
|
||||
{"non-existing client", email, chanId, wrong, manager.ErrNotFound},
|
||||
{"connected client", email, chanID, client.ID, nil},
|
||||
{"non-connected client", email, chanID, client.ID, clients.ErrNotFound},
|
||||
{"non-existing user", wrong, chanID, client.ID, clients.ErrNotFound},
|
||||
{"non-existing channel", email, wrong, client.ID, clients.ErrNotFound},
|
||||
{"non-existing client", email, chanID, wrong, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := chanRepo.Disconnect(tc.owner, tc.chanId, tc.clientId)
|
||||
err := chanRepo.Disconnect(tc.owner, tc.chanID, tc.clientID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@ -217,33 +184,30 @@ func TestChannelDisconnect(t *testing.T) {
|
||||
func TestChannelAccessCheck(t *testing.T) {
|
||||
email := "channel-access-check@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
client := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
|
||||
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
|
||||
|
||||
chanRepo.Connect(email, chanId, client.ID)
|
||||
chanRepo.Connect(email, chanID, client.ID)
|
||||
|
||||
cases := map[string]struct {
|
||||
chanId string
|
||||
clientId string
|
||||
chanID string
|
||||
clientID string
|
||||
hasAccess bool
|
||||
}{
|
||||
"client that has access": {chanId, client.ID, true},
|
||||
"client without access": {chanId, wrong, false},
|
||||
"client that has access": {chanID, client.ID, true},
|
||||
"client without access": {chanID, wrong, false},
|
||||
"check access to non-existing channel": {wrong, client.ID, false},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
hasAccess := chanRepo.HasClient(tc.chanId, tc.clientId)
|
||||
hasAccess := chanRepo.HasClient(tc.chanID, tc.clientID)
|
||||
assert.Equal(t, tc.hasAccess, hasAccess, fmt.Sprintf("%s: expected %t got %t\n", desc, tc.hasAccess, hasAccess))
|
||||
}
|
||||
}
|
@ -3,11 +3,11 @@ package postgres
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres" // required by GORM
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
var _ manager.ClientRepository = (*clientRepository)(nil)
|
||||
var _ clients.ClientRepository = (*clientRepository)(nil)
|
||||
|
||||
type clientRepository struct {
|
||||
db *gorm.DB
|
||||
@ -15,37 +15,37 @@ type clientRepository struct {
|
||||
|
||||
// NewClientRepository instantiates a PostgreSQL implementation of client
|
||||
// repository.
|
||||
func NewClientRepository(db *gorm.DB) manager.ClientRepository {
|
||||
func NewClientRepository(db *gorm.DB) clients.ClientRepository {
|
||||
return &clientRepository{db}
|
||||
}
|
||||
|
||||
func (cr *clientRepository) Id() string {
|
||||
func (cr *clientRepository) ID() string {
|
||||
return uuid.NewV4().String()
|
||||
}
|
||||
|
||||
func (cr *clientRepository) Save(client manager.Client) error {
|
||||
func (cr *clientRepository) Save(client clients.Client) error {
|
||||
return cr.db.Create(&client).Error
|
||||
}
|
||||
|
||||
func (cr *clientRepository) Update(client manager.Client) error {
|
||||
func (cr *clientRepository) Update(client clients.Client) error {
|
||||
sql := "UPDATE clients SET name = ?, payload = ? WHERE owner = ? AND id = ?;"
|
||||
res := cr.db.Exec(sql, client.Name, client.Payload, client.Owner, client.ID)
|
||||
|
||||
if res.Error == nil && res.RowsAffected == 0 {
|
||||
return manager.ErrNotFound
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return res.Error
|
||||
}
|
||||
|
||||
func (cr *clientRepository) One(owner, id string) (manager.Client, error) {
|
||||
client := manager.Client{}
|
||||
func (cr *clientRepository) One(owner, id string) (clients.Client, error) {
|
||||
client := clients.Client{}
|
||||
|
||||
res := cr.db.First(&client, "owner = ? AND id = ?", owner, id)
|
||||
|
||||
if err := res.Error; err != nil {
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
return client, manager.ErrNotFound
|
||||
return client, clients.ErrNotFound
|
||||
}
|
||||
|
||||
return client, err
|
||||
@ -54,8 +54,8 @@ func (cr *clientRepository) One(owner, id string) (manager.Client, error) {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (cr *clientRepository) All(owner string, offset, limit int) []manager.Client {
|
||||
var clients []manager.Client
|
||||
func (cr *clientRepository) All(owner string, offset, limit int) []clients.Client {
|
||||
var clients []clients.Client
|
||||
|
||||
cr.db.Offset(offset).Limit(limit).Find(&clients, "owner = ?", owner)
|
||||
|
||||
@ -63,6 +63,6 @@ func (cr *clientRepository) All(owner string, offset, limit int) []manager.Clien
|
||||
}
|
||||
|
||||
func (cr *clientRepository) Remove(owner, id string) error {
|
||||
cr.db.Delete(&manager.Client{}, "owner = ? AND id = ?", owner, id)
|
||||
cr.db.Delete(&clients.Client{}, "owner = ? AND id = ?", owner, id)
|
||||
return nil
|
||||
}
|
@ -4,65 +4,42 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/mainflux/mainflux/clients/postgres"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClientSave(t *testing.T) {
|
||||
email := "client-save@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
|
||||
c1 := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
c2 := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: "unknown@example.com",
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
hasErr bool
|
||||
}{
|
||||
"new client, existing user": {c1, false},
|
||||
"new client, non-existing user": {c2, true},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
hasErr := clientRepo.Save(tc.client) != nil
|
||||
assert.Equal(t, tc.hasErr, hasErr, fmt.Sprintf("%s: expected %t got %t\n", desc, tc.hasErr, hasErr))
|
||||
}
|
||||
hasErr := clientRepo.Save(client) != nil
|
||||
assert.False(t, hasErr, fmt.Sprintf("create new client: expected false got %t\n", hasErr))
|
||||
}
|
||||
|
||||
func TestClientUpdate(t *testing.T) {
|
||||
email := "client-update@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
|
||||
c := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
c := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
clientRepo.Save(c)
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
client clients.Client
|
||||
err error
|
||||
}{
|
||||
"existing client": {c, nil},
|
||||
"non-existing client with existing user": {manager.Client{ID: wrong, Owner: email}, manager.ErrNotFound},
|
||||
"non-existing client with non-existing user": {manager.Client{ID: wrong, Owner: wrong}, manager.ErrNotFound},
|
||||
"non-existing client with existing user": {clients.Client{ID: wrong, Owner: email}, clients.ErrNotFound},
|
||||
"non-existing client with non-existing user": {clients.Client{ID: wrong, Owner: wrong}, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@ -74,13 +51,10 @@ func TestClientUpdate(t *testing.T) {
|
||||
func TestSingleClientRetrieval(t *testing.T) {
|
||||
email := "client-single-retrieval@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
|
||||
c := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
c := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
@ -92,8 +66,8 @@ func TestSingleClientRetrieval(t *testing.T) {
|
||||
err error
|
||||
}{
|
||||
"existing user": {c.Owner, c.ID, nil},
|
||||
"existing user, non-existing client": {c.Owner, wrong, manager.ErrNotFound},
|
||||
"non-existing owner": {wrong, c.ID, manager.ErrNotFound},
|
||||
"existing user, non-existing client": {c.Owner, wrong, clients.ErrNotFound},
|
||||
"non-existing owner": {wrong, c.ID, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@ -105,16 +79,13 @@ func TestSingleClientRetrieval(t *testing.T) {
|
||||
func TestMultiClientRetrieval(t *testing.T) {
|
||||
email := "client-multi-retrieval@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
|
||||
n := 10
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
c := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
c := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
@ -140,12 +111,9 @@ func TestMultiClientRetrieval(t *testing.T) {
|
||||
func TestClientRemoval(t *testing.T) {
|
||||
email := "client-removal@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
client := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
@ -157,8 +125,8 @@ func TestClientRemoval(t *testing.T) {
|
||||
t.Fatalf("#%d: failed to remove client due to: %s", i, err)
|
||||
}
|
||||
|
||||
if _, err := clientRepo.One(email, client.ID); err != manager.ErrNotFound {
|
||||
t.Fatalf("#%d: expected %s got %s", i, manager.ErrNotFound, err)
|
||||
if _, err := clientRepo.One(email, client.ID); err != clients.ErrNotFound {
|
||||
t.Fatalf("#%d: expected %s got %s", i, clients.ErrNotFound, err)
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres" // required by GORM
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
const errDuplicate string = "unique_violation"
|
||||
@ -30,15 +30,7 @@ func Connect(host, port, name, user, pass string) (*gorm.DB, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db = db.AutoMigrate(&manager.User{}, &manager.Client{}, &manager.Channel{}, &connection{})
|
||||
|
||||
db = db.Model(&manager.Client{}).
|
||||
AddForeignKey("owner", "users(email)", "CASCADE", "CASCADE").
|
||||
AddUniqueIndex("idx_pk_client", "id", "owner")
|
||||
|
||||
db = db.Model(&manager.Channel{}).
|
||||
AddForeignKey("owner", "users(email)", "CASCADE", "CASCADE").
|
||||
AddUniqueIndex("idx_pk_channel", "id", "owner")
|
||||
db = db.AutoMigrate(&clients.Client{}, &clients.Channel{}, &connection{})
|
||||
|
||||
db = db.Model(&connection{}).
|
||||
AddForeignKey("client_id", "clients(id)", "CASCADE", "CASCADE").
|
@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
"github.com/mainflux/mainflux/clients/postgres"
|
||||
"gopkg.in/ory-am/dockertest.v3"
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
package manager
|
||||
package clients
|
||||
|
||||
// Hasher specifies an API for generating hashes of an arbitrary textual
|
||||
// content.
|
@ -1,36 +1,27 @@
|
||||
package manager
|
||||
package clients
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrConflict indicates usage of the existing email during account
|
||||
// registration.
|
||||
ErrConflict error = errors.New("email already taken")
|
||||
ErrConflict = errors.New("email already taken")
|
||||
|
||||
// ErrMalformedEntity indicates malformed entity specification (e.g.
|
||||
// invalid username or password).
|
||||
ErrMalformedEntity error = errors.New("malformed entity specification")
|
||||
ErrMalformedEntity = errors.New("malformed entity specification")
|
||||
|
||||
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
|
||||
// when accessing a protected resource.
|
||||
ErrUnauthorizedAccess error = errors.New("missing or invalid credentials provided")
|
||||
ErrUnauthorizedAccess = errors.New("missing or invalid credentials provided")
|
||||
|
||||
// ErrNotFound indicates a non-existent entity request.
|
||||
ErrNotFound error = errors.New("non-existent entity")
|
||||
ErrNotFound = errors.New("non-existent entity")
|
||||
)
|
||||
|
||||
// Service specifies an API that must be fullfiled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type Service interface {
|
||||
// Register creates new user account. In case of the failed registration, a
|
||||
// non-nil error value is returned.
|
||||
Register(User) error
|
||||
|
||||
// Login authenticates the user given its credentials. Successful
|
||||
// authentication generates new access token. Failed invocations are
|
||||
// identified by the non-nil error values in the response.
|
||||
Login(User) (string, error)
|
||||
|
||||
// AddClient adds new client to the user identified by the provided key.
|
||||
AddClient(string, Client) (string, error)
|
||||
|
||||
@ -76,10 +67,7 @@ type Service interface {
|
||||
// clients.
|
||||
Disconnect(string, string, string) error
|
||||
|
||||
// Identity retrieves Client ID for provided client token.
|
||||
Identity(string) (string, error)
|
||||
|
||||
// CanAccess determines whether the channel can be accessed using the
|
||||
// provided key.
|
||||
// provided key and returns client's id.
|
||||
CanAccess(string, string) (string, error)
|
||||
}
|
@ -1,68 +1,13 @@
|
||||
swagger: "2.0"
|
||||
info:
|
||||
title: Mainflux manager service
|
||||
description: HTTP API for managing platform users, devices, applications and channels.
|
||||
title: Mainflux clients service
|
||||
description: HTTP API for managing platform devices, applications and channels.
|
||||
version: "1.0.0"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
paths:
|
||||
/users:
|
||||
post:
|
||||
summary: Registers user account
|
||||
description: |
|
||||
Registers new user account given email and password. New account will
|
||||
be uniquely identified by its email address.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- name: user
|
||||
description: JSON-formatted document describing the new user.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/User"
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
description: Registered new user.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
409:
|
||||
description: Failed due to using an existing email address.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/tokens:
|
||||
post:
|
||||
summary: User authentication
|
||||
description: |
|
||||
Generates an access token when provided with proper credentials.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- name: credentials
|
||||
description: JSON-formatted document containing user credentials.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/User"
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
description: User authenticated.
|
||||
schema:
|
||||
$ref: "#/definitions/Token"
|
||||
400:
|
||||
description: |
|
||||
Failed due to malformed JSON.
|
||||
403:
|
||||
description: |
|
||||
Failed due to using invalid credentials.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/clients:
|
||||
post:
|
||||
summary: Adds new client
|
||||
@ -358,49 +303,6 @@ paths:
|
||||
description: Channel or client does not exist.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/access-grant:
|
||||
get:
|
||||
summary: Checks the token validity
|
||||
description: |
|
||||
Internal endpoint used to verify requests forwarded by stateful adapters
|
||||
like MQTT. If the request is made using valid token, an identifier bound
|
||||
to that token will be returned.
|
||||
tags:
|
||||
- access control
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
responses:
|
||||
200:
|
||||
description: ID retrieved
|
||||
headers:
|
||||
X-client-id:
|
||||
type: string
|
||||
description: ID of the entity bound to the provided access key.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
/channels/{chanId}/access-grant:
|
||||
get:
|
||||
summary: Checks channel accessibility
|
||||
description: |
|
||||
Internal endpoint used to determine whether or not a channel can be
|
||||
accessed using credentials provided as part of the request.
|
||||
tags:
|
||||
- access control
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ChanId"
|
||||
responses:
|
||||
200:
|
||||
description: Client can access the channel.
|
||||
headers:
|
||||
X-client-id:
|
||||
type: string
|
||||
description: ID of the entity bound to the provided access key.
|
||||
403:
|
||||
description: Client cannot access the channel.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
@ -541,26 +443,3 @@ definitions:
|
||||
description: Arbitrary, string-encoded client's data.
|
||||
required:
|
||||
- type
|
||||
Token:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: Generated access token.
|
||||
required:
|
||||
- token
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
example: "test@example.com"
|
||||
description: User's email address will be used as its unique identifier
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
description: Free-form account password used for acquiring auth token(s).
|
||||
required:
|
||||
- email
|
||||
- password
|
164
cmd/clients/main.go
Normal file
164
cmd/clients/main.go
Normal file
@ -0,0 +1,164 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/mainflux/mainflux/clients/api"
|
||||
grpcapi "github.com/mainflux/mainflux/clients/api/grpc"
|
||||
httpapi "github.com/mainflux/mainflux/clients/api/http"
|
||||
"github.com/mainflux/mainflux/clients/bcrypt"
|
||||
"github.com/mainflux/mainflux/clients/jwt"
|
||||
"github.com/mainflux/mainflux/clients/postgres"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
usersapi "github.com/mainflux/mainflux/users/api/grpc"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "clients"
|
||||
defHTTPPort = "8180"
|
||||
defGRPCPort = "8181"
|
||||
defUsersURL = "localhost:8181"
|
||||
defSecret = "clients"
|
||||
envDBHost = "MF_CLIENTS_DB_HOST"
|
||||
envDBPort = "MF_CLIENTS_DB_PORT"
|
||||
envDBUser = "MF_CLIENTS_DB_USER"
|
||||
envDBPass = "MF_CLIENTS_DB_PASS"
|
||||
envDBName = "MF_CLIENTS_DB"
|
||||
envHTTPPort = "MF_CLIENTS_HTTP_PORT"
|
||||
envGRPCPort = "MF_CLIENTS_GRPC_PORT"
|
||||
envUsersURL = "MF_USERS_URL"
|
||||
envSecret = "MF_CLIENTS_SECRET"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
HTTPPort string
|
||||
GRPCPort string
|
||||
UsersURL string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
db := connectToDB(cfg, logger)
|
||||
defer db.Close()
|
||||
|
||||
conn := connectToUsersService(cfg.UsersURL, logger)
|
||||
defer conn.Close()
|
||||
|
||||
svc := newService(conn, db, cfg.Secret, logger)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(svc, cfg.HTTPPort, logger, errs)
|
||||
|
||||
go startGRPCServer(svc, cfg.GRPCPort, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err := <-errs
|
||||
logger.Error(fmt.Sprintf("Clients service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
return config{
|
||||
DBHost: mainflux.Env(envDBHost, defDBHost),
|
||||
DBPort: mainflux.Env(envDBPort, defDBPort),
|
||||
DBUser: mainflux.Env(envDBUser, defDBUser),
|
||||
DBPass: mainflux.Env(envDBPass, defDBPass),
|
||||
DBName: mainflux.Env(envDBName, defDBName),
|
||||
HTTPPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
GRPCPort: mainflux.Env(envGRPCPort, defGRPCPort),
|
||||
UsersURL: mainflux.Env(envUsersURL, defUsersURL),
|
||||
Secret: mainflux.Env(envSecret, defSecret),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToDB(cfg config, logger log.Logger) *gorm.DB {
|
||||
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func connectToUsersService(usersAddr string, logger log.Logger) *grpc.ClientConn {
|
||||
conn, err := grpc.Dial(usersAddr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
func newService(conn *grpc.ClientConn, db *gorm.DB, secret string, logger log.Logger) clients.Service {
|
||||
users := usersapi.NewClient(conn)
|
||||
clientsRepo := postgres.NewClientRepository(db)
|
||||
channelsRepo := postgres.NewChannelRepository(db)
|
||||
hasher := bcrypt.New()
|
||||
idp := jwt.New(secret)
|
||||
|
||||
svc := clients.New(users, clientsRepo, channelsRepo, hasher, idp)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "clients",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "clients",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
return svc
|
||||
}
|
||||
|
||||
func startHTTPServer(svc clients.Service, port string, logger log.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
logger.Info(fmt.Sprintf("Clients service started, exposed port %s", port))
|
||||
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc))
|
||||
}
|
||||
|
||||
func startGRPCServer(svc clients.Service, port string, logger log.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
listener, err := net.Listen("tcp", p)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to listen on port %s: %s", port, err))
|
||||
}
|
||||
server := grpc.NewServer()
|
||||
mainflux.RegisterClientsServiceServer(server, grpcapi.NewServer(svc))
|
||||
logger.Info(fmt.Sprintf("Clients gRPC service started, exposed port %s", port))
|
||||
errs <- server.Serve(listener)
|
||||
}
|
@ -8,12 +8,13 @@ import (
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
clientsapi "github.com/mainflux/mainflux/clients/api/grpc"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"github.com/mainflux/mainflux/coap/api"
|
||||
"github.com/mainflux/mainflux/coap/nats"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
@ -21,21 +22,21 @@ import (
|
||||
const (
|
||||
defPort int = 5683
|
||||
defNatsURL string = broker.DefaultURL
|
||||
defManagerURL string = "http://localhost:8180"
|
||||
defClientsURL string = "localhost:8181"
|
||||
envPort string = "MF_COAP_ADAPTER_PORT"
|
||||
envNatsURL string = "MF_NATS_URL"
|
||||
envManagerURL string = "MF_MANAGER_URL"
|
||||
envClientsURL string = "MF_CLIENTS_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ManagerURL string
|
||||
ClientsURL string
|
||||
NatsURL string
|
||||
Port int
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ManagerURL: mainflux.Env(envManagerURL, defManagerURL),
|
||||
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: defPort,
|
||||
}
|
||||
@ -49,6 +50,15 @@ func main() {
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
cc := clientsapi.NewClient(conn)
|
||||
|
||||
pubsub := nats.New(nc, logger)
|
||||
svc := coap.New(pubsub)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
@ -73,9 +83,8 @@ func main() {
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%d", cfg.Port)
|
||||
mc := manager.NewClient(cfg.ManagerURL)
|
||||
logger.Info(fmt.Sprintf("CoAP adapter service started, exposed port %d", cfg.Port))
|
||||
errs <- api.ListenAndServe(svc, mc, p, api.MakeHandler(svc))
|
||||
errs <- api.ListenAndServe(svc, cc, p, api.MakeHandler(svc))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
|
@ -9,33 +9,34 @@ import (
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
clientsapi "github.com/mainflux/mainflux/clients/api/grpc"
|
||||
adapter "github.com/mainflux/mainflux/http"
|
||||
"github.com/mainflux/mainflux/http/api"
|
||||
"github.com/mainflux/mainflux/http/nats"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
defPort string = "8180"
|
||||
defNatsURL string = broker.DefaultURL
|
||||
defManagerURL string = "http://localhost:8180"
|
||||
defClientsURL string = "localhost:8181"
|
||||
envPort string = "MF_HTTP_ADAPTER_PORT"
|
||||
envNatsURL string = "MF_NATS_URL"
|
||||
envManagerURL string = "MF_MANAGER_URL"
|
||||
envClientsURL string = "MF_CLIENTS_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ManagerURL string
|
||||
ClientsURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ManagerURL: mainflux.Env(envManagerURL, defManagerURL),
|
||||
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
@ -49,6 +50,14 @@ func main() {
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
cc := clientsapi.NewClient(conn)
|
||||
pub := nats.NewMessagePublisher(nc)
|
||||
|
||||
svc := adapter.New(pub)
|
||||
@ -73,9 +82,8 @@ func main() {
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
mc := manager.NewClient(cfg.ManagerURL)
|
||||
logger.Info(fmt.Sprintf("HTTP adapter service started, exposed port %s", cfg.Port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, cc))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
|
@ -1,108 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/api"
|
||||
"github.com/mainflux/mainflux/manager/bcrypt"
|
||||
"github.com/mainflux/mainflux/manager/jwt"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
defDBHost string = "localhost"
|
||||
defDBPort string = "5432"
|
||||
defDBUser string = "mainflux"
|
||||
defDBPass string = "mainflux"
|
||||
defDBName string = "manager"
|
||||
defPort string = "8180"
|
||||
defSecret string = "manager"
|
||||
envDBHost string = "MF_DB_HOST"
|
||||
envDBPort string = "MF_DB_PORT"
|
||||
envDBUser string = "MF_DB_USER"
|
||||
envDBPass string = "MF_DB_PASS"
|
||||
envDBName string = "MF_MANAGER_DB"
|
||||
envPort string = "MF_MANAGER_PORT"
|
||||
envSecret string = "MF_MANAGER_SECRET"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
Port string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
DBHost: mainflux.Env(envDBHost, defDBHost),
|
||||
DBPort: mainflux.Env(envDBPort, defDBPort),
|
||||
DBUser: mainflux.Env(envDBUser, defDBUser),
|
||||
DBPass: mainflux.Env(envDBPass, defDBPass),
|
||||
DBName: mainflux.Env(envDBName, defDBName),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
Secret: mainflux.Env(envSecret, defSecret),
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
users := postgres.NewUserRepository(db)
|
||||
clients := postgres.NewClientRepository(db)
|
||||
channels := postgres.NewChannelRepository(db)
|
||||
hasher := bcrypt.New()
|
||||
idp := jwt.New(cfg.Secret)
|
||||
|
||||
svc := manager.New(users, clients, channels, hasher, idp)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "manager",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "manager",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
logger.Info(fmt.Sprintf("Manager service started, exposed port %s", cfg.Port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("Manager service terminated: %s", err))
|
||||
}
|
145
cmd/users/main.go
Normal file
145
cmd/users/main.go
Normal file
@ -0,0 +1,145 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
"github.com/mainflux/mainflux/users/api"
|
||||
grpcapi "github.com/mainflux/mainflux/users/api/grpc"
|
||||
httpapi "github.com/mainflux/mainflux/users/api/http"
|
||||
"github.com/mainflux/mainflux/users/bcrypt"
|
||||
"github.com/mainflux/mainflux/users/jwt"
|
||||
"github.com/mainflux/mainflux/users/postgres"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "users"
|
||||
defHTTPPort = "8180"
|
||||
defGRPCPort = "8181"
|
||||
defSecret = "users"
|
||||
envDBHost = "MF_USERS_DB_HOST"
|
||||
envDBPort = "MF_USERS_DB_PORT"
|
||||
envDBUser = "MF_USERS_DB_USER"
|
||||
envDBPass = "MF_USERS_DB_PASS"
|
||||
envDBName = "MF_USERS_DB"
|
||||
envHTTPPort = "MF_USERS_HTTP_PORT"
|
||||
envGRPCPort = "MF_USERS_GRPC_PORT"
|
||||
envSecret = "MF_USERS_SECRET"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
HTTPPort string
|
||||
GRPCPort string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
db := connectToDB(cfg, logger)
|
||||
defer db.Close()
|
||||
|
||||
svc := newService(db, cfg.Secret, logger)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(svc, cfg.HTTPPort, logger, errs)
|
||||
|
||||
go startGRPCServer(svc, cfg.GRPCPort, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err := <-errs
|
||||
logger.Error(fmt.Sprintf("Users service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
return config{
|
||||
DBHost: mainflux.Env(envDBHost, defDBHost),
|
||||
DBPort: mainflux.Env(envDBPort, defDBPort),
|
||||
DBUser: mainflux.Env(envDBUser, defDBUser),
|
||||
DBPass: mainflux.Env(envDBPass, defDBPass),
|
||||
DBName: mainflux.Env(envDBName, defDBName),
|
||||
HTTPPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
GRPCPort: mainflux.Env(envGRPCPort, defGRPCPort),
|
||||
Secret: mainflux.Env(envSecret, defSecret),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToDB(cfg config, logger log.Logger) *gorm.DB {
|
||||
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func newService(db *gorm.DB, secret string, logger log.Logger) users.Service {
|
||||
repo := postgres.New(db)
|
||||
hasher := bcrypt.New()
|
||||
idp := jwt.New(secret)
|
||||
|
||||
svc := users.New(repo, hasher, idp)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "users",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "users",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
return svc
|
||||
}
|
||||
|
||||
func startHTTPServer(svc users.Service, port string, logger log.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
logger.Info(fmt.Sprintf("Users HTTP service started, exposed port %s", port))
|
||||
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc, logger))
|
||||
}
|
||||
|
||||
func startGRPCServer(svc users.Service, port string, logger log.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
listener, err := net.Listen("tcp", p)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to listen on port %s: %s", port, err))
|
||||
}
|
||||
server := grpc.NewServer()
|
||||
mainflux.RegisterUsersServiceServer(server, grpcapi.NewServer(svc))
|
||||
logger.Info(fmt.Sprintf("Users gRPC service started, exposed port %s", port))
|
||||
errs <- server.Serve(listener)
|
||||
}
|
@ -9,33 +9,34 @@ import (
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
clientsapi "github.com/mainflux/mainflux/clients/api/grpc"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
adapter "github.com/mainflux/mainflux/ws"
|
||||
"github.com/mainflux/mainflux/ws/api"
|
||||
"github.com/mainflux/mainflux/ws/nats"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
defPort = "8180"
|
||||
defNatsURL = broker.DefaultURL
|
||||
defManagerURL = "http://localhost:8180"
|
||||
defClientsURL = "localhost:8181"
|
||||
envPort = "MF_WS_ADAPTER_PORT"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envManagerURL = "MF_MANAGER_URL"
|
||||
envClientsURL = "MF_CLIENTS_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ManagerURL string
|
||||
ClientsURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ManagerURL: mainflux.Env(envManagerURL, defManagerURL),
|
||||
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
@ -49,6 +50,14 @@ func main() {
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
cc := clientsapi.NewClient(conn)
|
||||
pubsub := nats.New(nc)
|
||||
svc := adapter.New(pubsub)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
@ -72,9 +81,8 @@ func main() {
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
mc := manager.NewClient(cfg.ManagerURL)
|
||||
logger.Info(fmt.Sprintf("WebSocket adapter service started, exposed port %s", cfg.Port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc, logger))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, cc, logger))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
|
@ -1,14 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mux "github.com/dereulenspiegel/coap-mux"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
|
||||
gocoap "github.com/dustin/go-coap"
|
||||
)
|
||||
@ -30,7 +31,7 @@ func authKey(opt interface{}) (string, error) {
|
||||
return arr[1], nil
|
||||
}
|
||||
|
||||
func authorize(msg *gocoap.Message, res *gocoap.Message, cid string) (publisher string, err error) {
|
||||
func authorize(msg *gocoap.Message, res *gocoap.Message, cid string) (publisher *mainflux.Identity, err error) {
|
||||
// Device Key is passed as Uri-Query parameter, which option ID is 15 (0xf).
|
||||
key, err := authKey(msg.Option(gocoap.URIQuery))
|
||||
if err != nil {
|
||||
@ -43,14 +44,13 @@ func authorize(msg *gocoap.Message, res *gocoap.Message, cid string) (publisher
|
||||
return
|
||||
}
|
||||
|
||||
publisher, err = auth.CanAccess(cid, key)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
publisher, err = auth.CanAccess(ctx, &mainflux.AccessReq{key, cid})
|
||||
|
||||
if err != nil {
|
||||
switch err {
|
||||
case manager.ErrServiceUnreachable:
|
||||
res.Code = gocoap.ServiceUnavailable
|
||||
default:
|
||||
res.Code = gocoap.Unauthorized
|
||||
}
|
||||
res.Code = gocoap.Unauthorized
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -103,8 +103,8 @@ func serve(svc coap.Service, conn *net.UDPConn, data []byte, addr *net.UDPAddr,
|
||||
}
|
||||
|
||||
// ListenAndServe binds to the given address and serve requests forever.
|
||||
func ListenAndServe(svc coap.Service, mgr manager.ManagerClient, addr string, rh gocoap.Handler) error {
|
||||
auth = mgr
|
||||
func ListenAndServe(svc coap.Service, csc mainflux.ClientsServiceClient, addr string, rh gocoap.Handler) error {
|
||||
auth = csc
|
||||
uaddr, err := net.ResolveUDPAddr(network, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"github.com/mainflux/mainflux/coap/nats"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
|
||||
"math/rand"
|
||||
|
||||
@ -22,7 +21,7 @@ import (
|
||||
var (
|
||||
errBadRequest = errors.New("bad request")
|
||||
errBadOption = errors.New("bad option")
|
||||
auth manager.ManagerClient
|
||||
auth mainflux.ClientsServiceClient
|
||||
)
|
||||
|
||||
const (
|
||||
@ -86,7 +85,7 @@ func receive(svc coap.Service) handler {
|
||||
|
||||
rawMsg := mainflux.RawMessage{
|
||||
Channel: cid,
|
||||
Publisher: publisher,
|
||||
Publisher: publisher.GetValue(),
|
||||
Protocol: protocol,
|
||||
Payload: msg.Payload,
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM golang:1.9-alpine AS builder
|
||||
FROM golang:1.10-alpine AS builder
|
||||
ARG SVC_NAME
|
||||
|
||||
WORKDIR /go/src/github.com/mainflux/mainflux
|
||||
@ -6,6 +6,7 @@ COPY . .
|
||||
RUN apk update \
|
||||
&& apk add make protobuf git \
|
||||
&& go get -u github.com/golang/protobuf/protoc-gen-go \
|
||||
&& go get -u google.golang.org/grpc \
|
||||
&& make $SVC_NAME \
|
||||
&& mv build/mainflux-$SVC_NAME /exe
|
||||
|
||||
|
@ -4,11 +4,11 @@ Mainflux IoT platform is comprised of the following services:
|
||||
|
||||
| Service | Description |
|
||||
|:--------------------------------------------------------------------------|:------------------------------------------------------------------------|
|
||||
| [manager](https://github.com/mainflux/mainflux/tree/master/manager) | Manages platform entities, and auth concerns |
|
||||
| [http-adapter](https://github.com/mainflux/mainflux/tree/master/http) | Provides an HTTP interface for accessing communication channels |
|
||||
| [users](https://github.com/mainflux/mainflux/tree/master/users) | Manages platform's users and auth concerns |
|
||||
| [clients](https://github.com/mainflux/mainflux/tree/master/clients) | Manages platform's clients, channels and access policies |
|
||||
| [normalizer](https://github.com/mainflux/mainflux/tree/master/normalizer) | Normalizes SenML messages and generates the "processed" messages stream |
|
||||
|
||||
> The following diagram is an (obsolete) overview of platform architecture
|
||||
| [http-adapter](https://github.com/mainflux/mainflux/tree/master/http) | Provides an HTTP interface for accessing communication channels |
|
||||
| [ws-adapter](https://github.com/mainflux/mainflux/tree/master/ws) | Provides a WebSocket interface for accessing communication channels |
|
||||
|
||||

|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 63 KiB |
1
docs/img/architecture.xml
Normal file
1
docs/img/architecture.xml
Normal file
@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:59.0) Gecko/20100101 Firefox/59.0" version="8.6.1" editor="www.draw.io" type="device"><diagram id="82652b06-2c2b-a339-9c87-b7b8398097a5" name="Page-1">7Vpbc5s4FP41fmyGO/gxdtN0Z7IZ7yY7bZ8yMshYW4EYIcd2f/1KIGFAOGETfEmTZDKBgy7wne87OrqM7GmyuaYgW/5JIohHlhFtRvbnkWWZjm3wf8KyLS1jyywNMUWRLLQz3KFfUBplvXiFIpg3CjJCMENZ0xiSNIUha9gApWTdLLYguNlrBmKoGe5CgHXrNxSxZWkNXGNn/wpRvFQ9m4Z8Mgfhz5iSVSr7G1n2ovgpHydAtSXL50sQkXXNZF+N7CklhJVXyWYKscBWwVbW+7LnafXeFKasTwW/rPAI8Ep++u31H7ff5cuxrQLkEVKGOD43YA7xjOSIIZLyR3PCGElG9gTkWemIBdpA3vhkyRLMb01+qSpfYhSLSoxk3JozSn7CKcGEcltKUt7VhKwYRim3Kr8aouQSZOI9kk0s6HYRIkbR5iKjZLN9yCHl7fNS+qdLNET3cFMzSSiuIUkgo1teRD6t+Cdpa3lueb/ekcD3LqRxWWPAWFYEknhx1fYOfH4h8d/jPE9zxj/863LNGVg4gX/xeokYvMtAKOxrDk0TdpSULEdJzLsOMeLYMH51DQF9MK1gw/8usjTmRRcIY+UJTtkIwGARaj7iT7wwgPPFMGjb4ybappJXDW1TCaUOtjcA2LapgX3992yqYQ0jHhjkLaFsSWKSAny1s04KtQvKF1yt4Q83iH0XZk6Y8u6HLMQxo9vaI3H7Q9bKGffSpYhh3BBikOcoVOYvCKvG/4WMbWXgBCtGhHaq17shQmFFub1+ysmKhlDxXsZYQGMoi1nj0iYAeNKbFGLA0GMzcr7GNZbumhkljISEl/Iwf7/JnPKrWFyBCGTsHYnEMY4oEtc+jB4Cv6GISh8zSBF/SRHOy8DfTwswjfRC3Hgwudi6XPxTqcV1NLWM3EmmBON+1lzImceanqEwR7/AvCggcM8ISlnxTu5EtMBlJofukONSjLbtMT1BUVS4v9DdpEqDarKQiVAXyIpomi6qFE++XSNN6tLLJ+PCMA05QvcGWzY3E99dK0IWi5w7uO2Nqtd+4czWHDTFCAqANXFhHn9yWEt5QkxW0fNRrBWaoBm50O8KTWPPt4E3TGhqJ0t+z8gUDBCZOhJXnpWymMK7v240XCs0tzy5jAr+PgPovAxiN/M9CHN8g8jpQjiw5rY3EMKu14I40CEeHypBsj4SpF3EH+sRX6XrJ0iQxr0jym+aBXUJ4XBTBVcfYTUVdKUge5haV0DF8zrLS9L3Iqav81IVe5aXNezcDuiU7aWDqJpT260Etu2S8nNkrfpSRbuhoNWQ2WqoBEFr6AVDtq0L7O3lVBVtX51T8ZTKMoIG+J/M15Fj0AzL1hdOzlqgfWfWxxGo2RKo579QoGZLoJ51OIEGmse/3t/P7t6ELhVbB5nruPYr5zqbzgrD6FJP0ru9dEbi7J3VfYhzn9sdff3uGB5Xswk1s/ifXOhK8Xuv6nyQYS8Z9NWPdzBR79rKONRE3elYADx/uak4W5eb4sqZyM21mk612yrpKzfXf6ahAeWmz1VvL+/1EbdUg9rbtppub++3LjBZh0tOlosIUZ6fPESAgT3SbMlv4YrfzuWB4mcg+bluA2DH1Pdtqy36OlPsAfTn+brcBlgT67VBUgnU1AUagXxZ9KAtmsm99vqKmXGCbRQn0EOAd7JFNUefUrxD5bimPnAdTDlOxzJLr5GrQy/6MPTk4CVu22LaL5j9q8y9iN61va4C9ZmMdW8xtVSffnz6NJjSFZf7xb/zokA73XGcF1LAN1oNtWPFgBQIDjL2NvajGolwxzjbGUuOPN42CNmLfG7H2ocaAk9wiKHjHCKhCcD806nm4t9kU0vTyTGP9nj6mvl0lYuznNoRq5Ck+So5yRmrRRDCsNMR88B13CdD7SvOWI07pg8DOYLf7k73lhFvd4TavvoP</diagram></mxfile>
|
@ -10,7 +10,7 @@ default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------------------|---------------------|-----------------------|
|
||||
| MF_MANAGER_URL | Manager service URL | http://localhost:8180 |
|
||||
| MF_CLIENTS_URL | Clients service URL | localhost:8181 |
|
||||
| MF_NATS_URL | NATS instance URL | nats://localhost:4222 |
|
||||
| MF_HTTP_ADAPTER_PORT | Service HTTP port | 8180 |
|
||||
|
||||
@ -28,7 +28,7 @@ services:
|
||||
ports:
|
||||
- [host machine port]:8180
|
||||
environment:
|
||||
MF_MANAGER_URL: [Manager service URL]
|
||||
MF_CLIENTS_URL: [Clients service URL]
|
||||
MF_NATS_URL: [NATS instance URL]
|
||||
MF_HTTP_ADAPTER_PORT: [Service HTTP port]
|
||||
```
|
||||
@ -48,7 +48,7 @@ make http
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_MANAGER_URL=[Manager service URL] MF_NATS_URL=[NATS instance URL] MF_HTTP_ADAPTER_PORT=[Service HTTP port] $GOBIN/mainflux-http
|
||||
MF_CLIENTS_URL=[Clients service URL] MF_NATS_URL=[NATS instance URL] MF_HTTP_ADAPTER_PORT=[Service HTTP port] $GOBIN/mainflux-http
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
adapter "github.com/mainflux/mainflux/http"
|
||||
"github.com/mainflux/mainflux/http/api"
|
||||
"github.com/mainflux/mainflux/http/mocks"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -28,23 +27,13 @@ func newService() mainflux.MessagePublisher {
|
||||
return adapter.New(pub)
|
||||
}
|
||||
|
||||
func newHTTPServer(pub mainflux.MessagePublisher, mc manager.ManagerClient) *httptest.Server {
|
||||
mux := api.MakeHandler(pub, mc)
|
||||
func newHTTPServer(pub mainflux.MessagePublisher, cc mainflux.ClientsServiceClient) *httptest.Server {
|
||||
mux := api.MakeHandler(pub, cc)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func newManagerServer() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") == "invalid_token" {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
}
|
||||
|
||||
func newManagerClient(url string) manager.ManagerClient {
|
||||
return manager.NewClient(url)
|
||||
func newClientsClient() mainflux.ClientsServiceClient {
|
||||
return mocks.NewClientsClient(map[string]string{token: id})
|
||||
}
|
||||
|
||||
type testRequest struct {
|
||||
@ -71,12 +60,10 @@ func (tr testRequest) make() (*http.Response, error) {
|
||||
}
|
||||
|
||||
func TestPublish(t *testing.T) {
|
||||
mcServer := newManagerServer()
|
||||
defer mcServer.Close()
|
||||
mc := newManagerClient(mcServer.URL)
|
||||
clientsClient := newClientsClient()
|
||||
|
||||
pub := newService()
|
||||
ts := newHTTPServer(pub, mc)
|
||||
ts := newHTTPServer(pub, clientsClient)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
@ -3,30 +3,32 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const protocol string = "http"
|
||||
|
||||
var (
|
||||
errMalformedData error = errors.New("malformed SenML data")
|
||||
errNotFound error = errors.New("non-existent entity")
|
||||
auth manager.ManagerClient
|
||||
errMalformedData = errors.New("malformed SenML data")
|
||||
errNotFound = errors.New("non-existent entity")
|
||||
auth mainflux.ClientsServiceClient
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc mainflux.MessagePublisher, mc manager.ManagerClient) http.Handler {
|
||||
auth = mc
|
||||
func MakeHandler(svc mainflux.MessagePublisher, cc mainflux.ClientsServiceClient) http.Handler {
|
||||
auth = cc
|
||||
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
@ -73,7 +75,7 @@ func authorize(r *http.Request) (string, error) {
|
||||
apiKey := r.Header.Get("Authorization")
|
||||
|
||||
if apiKey == "" {
|
||||
return "", manager.ErrUnauthorizedAccess
|
||||
return "", clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
// extract ID from /channels/:id/messages
|
||||
@ -82,12 +84,15 @@ func authorize(r *http.Request) (string, error) {
|
||||
return "", errNotFound
|
||||
}
|
||||
|
||||
id, err := auth.CanAccess(c, apiKey)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := auth.CanAccess(ctx, &mainflux.AccessReq{Token: apiKey, ChanID: c})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
return id.GetValue(), nil
|
||||
}
|
||||
|
||||
func decodePayload(body io.ReadCloser) ([]byte, error) {
|
||||
@ -111,9 +116,10 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errNotFound:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case manager.ErrUnauthorizedAccess:
|
||||
case clients.ErrUnauthorizedAccess:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
default:
|
||||
fmt.Println(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
34
http/mocks/clients.go
Normal file
34
http/mocks/clients.go
Normal file
@ -0,0 +1,34 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.ClientsServiceClient = (*clientsClient)(nil)
|
||||
|
||||
type clientsClient struct {
|
||||
clients map[string]string
|
||||
}
|
||||
|
||||
// NewClientsClient returns mock implementation of clients service client.
|
||||
func NewClientsClient(data map[string]string) mainflux.ClientsServiceClient {
|
||||
return &clientsClient{data}
|
||||
}
|
||||
|
||||
func (client clientsClient) CanAccess(ctx context.Context, req *mainflux.AccessReq, opts ...grpc.CallOption) (*mainflux.Identity, error) {
|
||||
key := req.GetToken()
|
||||
if key == "" {
|
||||
return nil, clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
id, ok := client.clients[key]
|
||||
if !ok {
|
||||
return nil, clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return &mainflux.Identity{id}, nil
|
||||
}
|
24
internal.proto
Normal file
24
internal.proto
Normal file
@ -0,0 +1,24 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package mainflux;
|
||||
|
||||
service ClientsService {
|
||||
rpc CanAccess(AccessReq) returns (Identity) {}
|
||||
}
|
||||
|
||||
service UsersService {
|
||||
rpc Identify(Token) returns (mainflux.Identity) {}
|
||||
}
|
||||
|
||||
message AccessReq {
|
||||
string token = 1;
|
||||
string chanID = 2;
|
||||
}
|
||||
|
||||
message Token {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message Identity {
|
||||
string value = 1;
|
||||
}
|
129
k8s/README.md
129
k8s/README.md
@ -3,105 +3,94 @@ Scripts to deploy Mainflux on Kubernetes (https://kubernetes.io). Work in progre
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Setup PosgreSQL
|
||||
### 1. Setup NATS
|
||||
|
||||
- Create Persistent Volume for PosgreSQL to store data to.
|
||||
- Update `nats.conf` according to your needs.
|
||||
|
||||
- Create Kubernetes configmap to store NATS configuration:
|
||||
|
||||
```bash
|
||||
kubectl create -f 1-mainflux-postgres-persistence.yml
|
||||
```
|
||||
|
||||
- Claim Persistent Volume
|
||||
|
||||
```bash
|
||||
kubectl create -f 2-mainflux-postgres-claim.yml
|
||||
```
|
||||
|
||||
- Create PosgreSQL Pod
|
||||
|
||||
```bash
|
||||
kubectl create -f 3-mainflux-postgres-pod.yml
|
||||
```
|
||||
|
||||
- Create PosgreSQL Service
|
||||
|
||||
```bash
|
||||
kubectl create -f 4-mainflux-postgres-service.yml
|
||||
```
|
||||
|
||||
### 2. Setup NATS
|
||||
|
||||
- Change `nats.conf` according to your needs.
|
||||
|
||||
Create a Kubernetes configmap to store it:
|
||||
|
||||
```bash
|
||||
kubectl create configmap nats-config --from-file nats.conf
|
||||
kubectl create configmap nats-config --from-file k8s/nats/nats.conf
|
||||
```
|
||||
|
||||
- Deploy NATS:
|
||||
|
||||
```bash
|
||||
kubectl create -f nats.yml
|
||||
```
|
||||
kubectl create -f k8s/nats/nats.yml
|
||||
```
|
||||
|
||||
### 3. Setup Mainflux Services
|
||||
### 2. Setup Users service
|
||||
|
||||
- Create Manager Service
|
||||
|
||||
```bash
|
||||
kubectl create -f 1-mainflux-manager.yml
|
||||
```
|
||||
|
||||
- Create HTTP Service
|
||||
|
||||
```bash
|
||||
kubectl create -f 2-mainflux-http.yml
|
||||
- Deploy PostgreSQL service for Users service to use:
|
||||
|
||||
```
|
||||
|
||||
- Create CoAP Service
|
||||
|
||||
```bash
|
||||
kubectl create -f 4-mainflux-coap.yml
|
||||
kubectl create -f k8s/mainflux/users-postgres.yml
|
||||
```
|
||||
|
||||
- Create Normalizer Service
|
||||
- Deploy Users service:
|
||||
|
||||
```bash
|
||||
kubectl create -f 5-mainflux-normalizer.yml
|
||||
```
|
||||
kubectl create -f k8s/mainflux/users.yml
|
||||
```
|
||||
|
||||
### 4. Setup Dashflux Services
|
||||
### 3. Setup Clients service
|
||||
|
||||
- Create Dashflux Deployment and Service
|
||||
- Deploy PostgreSQL service for Clients service to use:
|
||||
|
||||
```bash
|
||||
kubectl create -f mainflux-dashflux.yaml
|
||||
```
|
||||
kubectl create -f k8s/mainflux/clients-postgres.yml
|
||||
```
|
||||
|
||||
### 5. Setup NginX Reverse Proxy for Mainflux Services
|
||||
- Deploy Clients service:
|
||||
|
||||
- Create TLS server side certificate and keys
|
||||
|
||||
```bash
|
||||
cd certs
|
||||
kubectl create secret tls mainflux-secret --key mainflux-server.key --cert mainflux-server.crt
|
||||
```
|
||||
kubectl create -f k8s/mainflux/clients.yml
|
||||
```
|
||||
|
||||
- Create Config Map based on the default.conf file.
|
||||
### 4. Setup Normalizer service
|
||||
|
||||
```bash
|
||||
cd ..
|
||||
kubectl create configmap mainflux-nginx-config --from-file=default.conf
|
||||
- Deploy Normalizer service:
|
||||
|
||||
```
|
||||
kubectl create -f k8s/mainflux/normalizer.yml
|
||||
```
|
||||
|
||||
- Create Deployment and Service from mainflux-dashflux.yaml file.
|
||||
### 5. Setup adapter services
|
||||
|
||||
```bash
|
||||
kubectl create -f mainflux-nginx.yaml
|
||||
- Deploy adapter service:
|
||||
|
||||
```
|
||||
kubectl create -f k8s/mainflux/<adapter_service_name>.yml
|
||||
```
|
||||
|
||||
### 6. Configure Internet Access
|
||||
### 6. Setup Dashflux
|
||||
|
||||
- Deploy Dashflux service:
|
||||
|
||||
```
|
||||
kubectl create -f k8s/mainflux/dashflux.yml
|
||||
```
|
||||
|
||||
### 7. Setup NginX Reverse Proxy for Mainflux Services
|
||||
|
||||
- Create TLS server side certificate and keys:
|
||||
|
||||
```
|
||||
kubectl create secret tls mainflux-secret --key k8s/nginx/certs/mainflux-server.key --cert k8s/nginx/certs/mainflux-server.crt
|
||||
```
|
||||
|
||||
- Create Kubernetes configmap to store NginX configuration:
|
||||
|
||||
```
|
||||
kubectl create configmap mainflux-nginx-config --from-file=k8s/nginx/default.conf
|
||||
```
|
||||
|
||||
- Deploy NginX service:
|
||||
|
||||
```
|
||||
kubectl create -f k8s/nginx/nginx.yml
|
||||
```
|
||||
|
||||
### 8. Configure Internet Access
|
||||
|
||||
Configure NAT on your Firewall to forward ports 80 (HTTP) and 443 (HTTPS) to mainflux-nginx service
|
||||
|
@ -1,44 +0,0 @@
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: manager
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: manager
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: manager
|
||||
spec:
|
||||
containers:
|
||||
- name: manager
|
||||
image: mainflux/manager:latest
|
||||
ports:
|
||||
- containerPort: 8180
|
||||
env:
|
||||
- name: MF_DB_HOST
|
||||
value: "mainflux-postgres"
|
||||
- name: MF_MANAGER_DB
|
||||
value: "mainflux"
|
||||
- name: MF_MANAGER_PORT
|
||||
value: "8180"
|
||||
- name: MF_MANAGER_SECRET
|
||||
value: "test-secret"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: manager
|
||||
labels:
|
||||
app: manager
|
||||
spec:
|
||||
ports:
|
||||
- port: 8180
|
||||
targetPort: 8180
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: manager
|
||||
type: LoadBalancer
|
66
k8s/mainflux/clients-postgres.yml
Normal file
66
k8s/mainflux/clients-postgres.yml
Normal file
@ -0,0 +1,66 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: mainflux-clients-postgres-data-disk
|
||||
labels:
|
||||
name: mainflux-clients-postgres-data-disk
|
||||
spec:
|
||||
capacity:
|
||||
storage: 10Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
hostPath:
|
||||
path: /tmp/data/clients-postgres-0
|
||||
persistentVolumeReclaimPolicy: Recycle
|
||||
---
|
||||
kind: PersistentVolumeClaim
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: mainflux-clients-postgres-data-claim
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: mainflux-clients-postgres
|
||||
labels:
|
||||
name: mainflux-clients-postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: mainflux-clients-postgres
|
||||
image: postgres:10.2-alpine
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: "mainflux"
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: "mainflux"
|
||||
- name: POSTGRES_DB
|
||||
value: "clients"
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/mainflux-clients-postgres-data
|
||||
ports:
|
||||
- containerPort: 5433
|
||||
volumeMounts:
|
||||
- mountPath: /var/lib/postgresql/data
|
||||
name: mainflux-clients-postgres-data
|
||||
volumes:
|
||||
- name: mainflux-clients-postgres-data
|
||||
persistentVolumeClaim:
|
||||
claimName: mainflux-clients-postgres-data-claim
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mainflux-clients-postgres
|
||||
labels:
|
||||
name: mainflux-clients-postgres
|
||||
spec:
|
||||
ports:
|
||||
- port: 5433
|
||||
selector:
|
||||
name: mainflux-clients-postgres
|
55
k8s/mainflux/clients.yml
Normal file
55
k8s/mainflux/clients.yml
Normal file
@ -0,0 +1,55 @@
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: clients
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
component: clients
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: clients
|
||||
spec:
|
||||
containers:
|
||||
- name: clients
|
||||
image: mainflux/clients:latest
|
||||
ports:
|
||||
- containerPort: 8182
|
||||
name: clients-http
|
||||
- containerPort: 8183
|
||||
name: clients-grpc
|
||||
env:
|
||||
- name: MF_CLIENTS_DB_HOST
|
||||
value: "mainflux-clients-postgres"
|
||||
- name: MF_CLIENTS_DB_PORT
|
||||
value: "5433"
|
||||
- name: MF_CLIENTS_HTTP_PORT
|
||||
value: "8182"
|
||||
- name: MF_CLIENTS_GRPC_PORT
|
||||
value: "8183"
|
||||
- name: MF_USERS_URL
|
||||
value: "users:8181"
|
||||
- name: MF_CLIENTS_SECRET
|
||||
value: "test-secret"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: clients
|
||||
labels:
|
||||
component: clients
|
||||
spec:
|
||||
ports:
|
||||
- port: 8182
|
||||
targetPort: 8182
|
||||
protocol: TCP
|
||||
name: http
|
||||
- port: 8183
|
||||
targetPort: 8183
|
||||
protocol: TCP
|
||||
name: grpc
|
||||
selector:
|
||||
component: clients
|
||||
type: LoadBalancer
|
@ -5,7 +5,6 @@ metadata:
|
||||
labels:
|
||||
component: http-adapter
|
||||
spec:
|
||||
serviceName: http-adapter
|
||||
replicas: 3
|
||||
template:
|
||||
metadata:
|
||||
@ -17,14 +16,14 @@ spec:
|
||||
image: mainflux/http:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8182
|
||||
- containerPort: 8185
|
||||
env:
|
||||
- name: MF_MANAGER_URL
|
||||
value: "http://manager:8180"
|
||||
- name: MF_CLIENTS_URL
|
||||
value: "clients:8181"
|
||||
- name: MF_NATS_URL
|
||||
value: "nats://nats:4222"
|
||||
- name: MF_HTTP_ADAPTER_PORT
|
||||
value: "8182"
|
||||
value: "8185"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
@ -36,7 +35,7 @@ spec:
|
||||
selector:
|
||||
component: http-adapter
|
||||
ports:
|
||||
- port: 8182
|
||||
targetPort: 8182
|
||||
- port: 8185
|
||||
targetPort: 8185
|
||||
type: LoadBalancer
|
||||
|
@ -5,7 +5,6 @@ metadata:
|
||||
labels:
|
||||
component: normalizer
|
||||
spec:
|
||||
serviceName: normalizer
|
||||
replicas: 3
|
||||
template:
|
||||
metadata:
|
||||
@ -20,7 +19,7 @@ spec:
|
||||
- name: MF_NATS_URL
|
||||
value: "nats://nats:4222"
|
||||
- name: MF_NORMALIZER_PORT
|
||||
value: "8181"
|
||||
value: "8184"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
@ -32,6 +31,6 @@ spec:
|
||||
selector:
|
||||
component: normalizer
|
||||
ports:
|
||||
- port: 8181
|
||||
targetPort: 8181
|
||||
- port: 8183
|
||||
targetPort: 8183
|
||||
clusterIP: None
|
66
k8s/mainflux/users-postgres.yml
Normal file
66
k8s/mainflux/users-postgres.yml
Normal file
@ -0,0 +1,66 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: mainflux-users-postgres-data-disk
|
||||
labels:
|
||||
name: mainflux-users-postgres-data-disk
|
||||
spec:
|
||||
capacity:
|
||||
storage: 10Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
hostPath:
|
||||
path: /tmp/data/users-postgres-0
|
||||
persistentVolumeReclaimPolicy: Recycle
|
||||
---
|
||||
kind: PersistentVolumeClaim
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: mainflux-users-postgres-data-claim
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: mainflux-users-postgres
|
||||
labels:
|
||||
name: mainflux-users-postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: mainflux-users-postgres
|
||||
image: postgres:10.2-alpine
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: "mainflux"
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: "mainflux"
|
||||
- name: POSTGRES_DB
|
||||
value: "users"
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/mainflux-users-postgres-data
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
volumeMounts:
|
||||
- mountPath: /var/lib/postgresql/data
|
||||
name: mainflux-users-postgres-data
|
||||
volumes:
|
||||
- name: mainflux-users-postgres-data
|
||||
persistentVolumeClaim:
|
||||
claimName: mainflux-users-postgres-data-claim
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mainflux-users-postgres
|
||||
labels:
|
||||
name: mainflux-users-postgres
|
||||
spec:
|
||||
ports:
|
||||
- port: 5432
|
||||
selector:
|
||||
name: mainflux-users-postgres
|
51
k8s/mainflux/users.yml
Normal file
51
k8s/mainflux/users.yml
Normal file
@ -0,0 +1,51 @@
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: users
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
component: users
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: users
|
||||
spec:
|
||||
containers:
|
||||
- name: users
|
||||
image: mainflux/users:latest
|
||||
ports:
|
||||
- containerPort: 8180
|
||||
name: users-http
|
||||
- containerPort: 8181
|
||||
name: users-grpc
|
||||
env:
|
||||
- name: MF_USERS_DB_HOST
|
||||
value: "mainflux-users-postgres"
|
||||
- name: MF_USERS_HTTP_PORT
|
||||
value: "8180"
|
||||
- name: MF_USERS_GRPC_PORT
|
||||
value: "8181"
|
||||
- name: MF_USERS_SECRET
|
||||
value: "test-secret"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: users
|
||||
labels:
|
||||
component: users
|
||||
spec:
|
||||
ports:
|
||||
- port: 8180
|
||||
targetPort: 8180
|
||||
protocol: TCP
|
||||
name: http
|
||||
- port: 8181
|
||||
targetPort: 8181
|
||||
protocol: TCP
|
||||
name: grpc
|
||||
selector:
|
||||
component: users
|
||||
type: LoadBalancer
|
@ -30,23 +30,11 @@ spec:
|
||||
labels:
|
||||
component: nats
|
||||
spec:
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: component
|
||||
operator: In
|
||||
values:
|
||||
- nats
|
||||
topologyKey: kubernetes.io/hostname
|
||||
containers:
|
||||
- name: nats
|
||||
image: nats:1.0.4
|
||||
args: [ "--config", "/etc/nats/nats.conf"]
|
||||
volumeMounts:
|
||||
- name: tls-volume
|
||||
mountPath: /etc/nats/tls
|
||||
- name: config-volume
|
||||
mountPath: /etc/nats
|
||||
ports:
|
||||
@ -63,9 +51,6 @@ spec:
|
||||
initialDelaySeconds: 10
|
||||
timeoutSeconds: 5
|
||||
volumes:
|
||||
- name: tls-volume
|
||||
secret:
|
||||
secretName: tls-nats-server
|
||||
- name: config-volume
|
||||
configMap:
|
||||
name: nats-config
|
||||
|
@ -25,12 +25,17 @@ ssl_prefer_server_ciphers on;
|
||||
# access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
# upstream k8s-manager {
|
||||
# server manager:8180;
|
||||
# }
|
||||
# upstream k8s-http {
|
||||
# server http-adapter:8182;
|
||||
# }
|
||||
upstream k8s-users {
|
||||
server users:8180;
|
||||
}
|
||||
|
||||
upstream k8s-clients {
|
||||
server clients:8182;
|
||||
}
|
||||
|
||||
upstream k8s-http {
|
||||
server http-adapter:8185;
|
||||
}
|
||||
|
||||
##
|
||||
# Virtual Host Configs
|
||||
@ -38,99 +43,127 @@ error_log /var/log/nginx/error.log;
|
||||
|
||||
# HTTP
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name mainflux-iot.ha.rs;
|
||||
access_log off;
|
||||
error_log off;
|
||||
return 301 https://$server_name$request_uri;
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name mainflux-iot.ha.rs;
|
||||
access_log off;
|
||||
error_log off;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS
|
||||
server {
|
||||
# SSL configuration
|
||||
#
|
||||
listen 443 ssl http2 default_server;
|
||||
listen [::]:443 ssl http2 default_server;
|
||||
# SSL configuration
|
||||
listen 443 ssl http2 default_server;
|
||||
listen [::]:443 ssl http2 default_server;
|
||||
|
||||
#
|
||||
# Note: You should disable gzip for SSL traffic.
|
||||
# See: https://bugs.debian.org/773332
|
||||
#
|
||||
# Read up on ssl_ciphers to ensure a secure configuration.
|
||||
# See: https://bugs.debian.org/765782
|
||||
#
|
||||
# Self signed certs generated by the ssl-cert package
|
||||
# Don't use them in a production server!
|
||||
#
|
||||
# include snippets/snakeoil.conf;
|
||||
#
|
||||
# Note: You should disable gzip for SSL traffic.
|
||||
# See: https://bugs.debian.org/773332
|
||||
#
|
||||
# Read up on ssl_ciphers to ensure a secure configuration.
|
||||
# See: https://bugs.debian.org/765782
|
||||
#
|
||||
# Self signed certs generated by the ssl-cert package
|
||||
# Don't use them in a production server!
|
||||
|
||||
# Certificates
|
||||
ssl_certificate /etc/nginx/ssl/tls.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/tls.key;
|
||||
#ssl_dhparam /etc/ssl/certs/dhparam.pem;
|
||||
# Certificates
|
||||
ssl_certificate /etc/nginx/ssl/tls.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/tls.key;
|
||||
|
||||
# from https://cipherli.st/
|
||||
# and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
|
||||
|
||||
# from https://cipherli.st/
|
||||
# and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||
ssl_ecdh_curve secp384r1;
|
||||
ssl_session_tickets off;
|
||||
ssl_stapling off;
|
||||
ssl_stapling_verify on;
|
||||
resolver 8.8.8.8 8.8.4.4 valid=300s;
|
||||
resolver_timeout 5s;
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||
ssl_ecdh_curve secp384r1;
|
||||
ssl_session_tickets off;
|
||||
ssl_stapling off;
|
||||
ssl_stapling_verify on;
|
||||
resolver 8.8.8.8 8.8.4.4 valid=300s;
|
||||
resolver_timeout 5s;
|
||||
# Disable preloading HSTS for now. You can use the commented out header line that includes
|
||||
# the "preload" directive if you understand the implications.
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header Access-Control-Allow-Origin '*';
|
||||
add_header Access-Control-Allow-Methods '*';
|
||||
add_header Access-Control-Allow-Headers "*";
|
||||
|
||||
# Disable preloading HSTS for now. You can use the commented out header line that includes
|
||||
# the "preload" directive if you understand the implications.
|
||||
#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header Access-Control-Allow-Origin '*';
|
||||
add_header Access-Control-Allow-Methods '*';
|
||||
add_header Access-Control-Allow-Headers "*";
|
||||
server_name mainflux-iot.ha.rs;
|
||||
|
||||
|
||||
server_name mainflux-iot.ha.rs;
|
||||
|
||||
# Proxy pass to manager service
|
||||
location /api/ {
|
||||
# Proxy pass to users service
|
||||
location ~ ^/api/(users|tokens)/(.*)$ {
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://manager:8180/;
|
||||
proxy_pass http://users:8180/$1/$2;
|
||||
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
}
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
|
||||
# Proxy pass to mainflux-http-adapter
|
||||
location /http/ {
|
||||
# Proxy pass to clients service
|
||||
location ~ ^/api/(clients|channels)/(.*)$ {
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://http-adapter:8182/;
|
||||
proxy_pass http://clients:8182/$1/$2;
|
||||
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
|
||||
# Proxy pass to api endpoint in users service
|
||||
location /api/ {
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://users:8180/;
|
||||
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
|
||||
# Proxy pass to mainflux-http-adapter
|
||||
location /http/ {
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://http-adapter:8185/;
|
||||
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
}
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
}
|
||||
location / {
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@ -140,9 +173,9 @@ server {
|
||||
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
}
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
}
|
@ -21,7 +21,6 @@ kind: ReplicationController
|
||||
metadata:
|
||||
name: mainflux-nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
@ -1,14 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: mainflux-postgres-data-disk
|
||||
labels:
|
||||
name: mainflux-postgres-data-disk
|
||||
spec:
|
||||
capacity:
|
||||
storage: 10Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
hostPath:
|
||||
path: /tmp/data/postgres-0
|
||||
persistentVolumeReclaimPolicy: Recycle
|
@ -1,10 +0,0 @@
|
||||
kind: PersistentVolumeClaim
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: mainflux-postgres-data-claim
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
@ -1,28 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: mainflux-postgres
|
||||
labels:
|
||||
name: mainflux-postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: mainflux-postgres
|
||||
image: postgres:10.2-alpine
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: "mainflux"
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: "mainflux"
|
||||
- name: POSTGRES_DB
|
||||
value: "mainflux"
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/mainflux-postgres-data
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
volumeMounts:
|
||||
- mountPath: /var/lib/postgresql/data
|
||||
name: mainflux-postgres-data
|
||||
volumes:
|
||||
- name: mainflux-postgres-data
|
||||
persistentVolumeClaim:
|
||||
claimName: mainflux-postgres-data-claim
|
@ -1,11 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mainflux-postgres
|
||||
labels:
|
||||
name: mainflux-postgres
|
||||
spec:
|
||||
ports:
|
||||
- port: 5432
|
||||
selector:
|
||||
name: mainflux-postgres
|
@ -1,78 +0,0 @@
|
||||
# Manager
|
||||
|
||||
Manager provides an HTTP API for managing platform resources: users, devices,
|
||||
applications and channels. Through this API clients are able to do the following
|
||||
actions:
|
||||
|
||||
- register new accounts and obtain access tokens
|
||||
- provision new clients (i.e. devices & applications)
|
||||
- create new channels
|
||||
- "connect" clients into the channels
|
||||
|
||||
For in-depth explanation of the aforementioned scenarios, as well as thorough
|
||||
understanding of Mainflux, please check out the [official documentation][doc].
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the
|
||||
following table. Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|-------------------|------------------------------------------|-----------|
|
||||
| MF_DB_HOST | Database host address | localhost |
|
||||
| MF_DB_PORT | Database host port | 5432 |
|
||||
| MF_DB_USER | Database user | mainflux |
|
||||
| MF_DB_PASSWORD | Database password | mainflux |
|
||||
| MF_MANAGER_DB | Name of the database used by the service | manager |
|
||||
| MF_MANAGER_PORT | Manager service HTTP port | 8180 |
|
||||
| MF_MANAGER_SECRET | string used for signing tokens | manager |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. The following snippet
|
||||
provides a compose file template that can be used to deploy the service container
|
||||
locally:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
services:
|
||||
manager:
|
||||
image: mainflux/manager:[version]
|
||||
container_name: [instance name]
|
||||
ports:
|
||||
- [host machine port]:[configured HTTP port]
|
||||
environment:
|
||||
MF_DB_HOST: [Database host address]
|
||||
MF_DB_PORT: [Database host port]
|
||||
MF_DB_USER: [Database user]
|
||||
MF_DB_PASS: [Database password]
|
||||
MF_MANAGER_DB: [Name of the database used by the service]
|
||||
MF_MANAGER_PORT: [Service HTTP port]
|
||||
MF_MANAGER_SECRET: [String used for signing tokens]
|
||||
```
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
go get github.com/mainflux/mainflux
|
||||
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux
|
||||
|
||||
# compile the manager
|
||||
make manager
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_DB_HOST=[Database host address] MF_DB_PORT=[Database host port] MF_DB_USER=[Database user] MF_DB_PASS=[Database password] MF_MANAGER_DB=[Name of the database used by the service] MF_MANAGER_PORT=[Service HTTP port] MF_MANAGER_SECRET=[String used for signing tokens] $GOBIN/mainflux-manager
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](swagger.yaml).
|
||||
|
||||
[doc]: http://mainflux.readthedocs.io
|
@ -1,772 +0,0 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/api"
|
||||
"github.com/mainflux/mainflux/manager/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
invalidEmail = "userexample.com"
|
||||
wrongID = "123e4567-e89b-12d3-a456-000000000042"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
)
|
||||
|
||||
var (
|
||||
user = manager.User{"user@example.com", "password"}
|
||||
client = manager.Client{Type: "app", Name: "test_app", Payload: "test_payload"}
|
||||
channel = manager.Channel{Name: "test"}
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService() manager.Service {
|
||||
users := mocks.NewUserRepository()
|
||||
clients := mocks.NewClientRepository()
|
||||
channels := mocks.NewChannelRepository(clients)
|
||||
hasher := mocks.NewHasher()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
|
||||
return manager.New(users, clients, channels, hasher, idp)
|
||||
}
|
||||
|
||||
func newServer(svc manager.Service) *httptest.Server {
|
||||
mux := api.MakeHandler(svc)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
data := toJSON(user)
|
||||
invalidData := toJSON(manager.User{Email: invalidEmail, Password: "password"})
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
status int
|
||||
}{
|
||||
{"register new user", data, contentType, http.StatusCreated},
|
||||
{"register existing user", data, contentType, http.StatusConflict},
|
||||
{"register user with invalid email address", invalidData, contentType, http.StatusBadRequest},
|
||||
{"register user with invalid request format", "{", contentType, http.StatusBadRequest},
|
||||
{"register user with empty JSON request", "{}", contentType, http.StatusBadRequest},
|
||||
{"register user with empty request", "", contentType, http.StatusBadRequest},
|
||||
{"register user with missing content type", data, "", http.StatusUnsupportedMediaType},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/users", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
tokenData := toJSON(map[string]string{"token": user.Email})
|
||||
data := toJSON(user)
|
||||
invalidEmailData := toJSON(manager.User{Email: invalidEmail, Password: "password"})
|
||||
invalidData := toJSON(manager.User{"user@example.com", "invalid_password"})
|
||||
nonexistentData := toJSON(manager.User{"non-existentuser@example.com", "pass"})
|
||||
svc.Register(user)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
status int
|
||||
res string
|
||||
}{
|
||||
{"login with valid credentials", data, contentType, http.StatusCreated, tokenData},
|
||||
{"login with invalid credentials", invalidData, contentType, http.StatusForbidden, ""},
|
||||
{"login with invalid email address", invalidEmailData, contentType, http.StatusBadRequest, ""},
|
||||
{"login non-existent user", nonexistentData, contentType, http.StatusForbidden, ""},
|
||||
{"login with invalid request format", "{", contentType, http.StatusBadRequest, ""},
|
||||
{"login with empty JSON request", "{}", contentType, http.StatusBadRequest, ""},
|
||||
{"login with empty request", "", contentType, http.StatusBadRequest, ""},
|
||||
{"login with missing content type", data, "", http.StatusUnsupportedMediaType, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/tokens", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
token := strings.Trim(string(body), "\n")
|
||||
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.res, token, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, token))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddClient(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
data := toJSON(client)
|
||||
invalidData := toJSON(manager.Client{
|
||||
Type: "foo",
|
||||
Name: "invalid_client",
|
||||
Payload: "some_payload",
|
||||
})
|
||||
svc.Register(user)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
location string
|
||||
}{
|
||||
{"add valid client", data, contentType, user.Email, http.StatusCreated, fmt.Sprintf("/clients/%s", id)},
|
||||
{"add client with invalid data", invalidData, contentType, user.Email, http.StatusBadRequest, ""},
|
||||
{"add client with invalid auth token", data, contentType, "invalid_token", http.StatusForbidden, ""},
|
||||
{"add client with invalid request format", "}", contentType, user.Email, http.StatusBadRequest, ""},
|
||||
{"add client with empty JSON request", "{}", contentType, user.Email, http.StatusBadRequest, ""},
|
||||
{"add client with empty request", "", contentType, user.Email, http.StatusBadRequest, ""},
|
||||
{"add client with missing content type", data, "", user.Email, http.StatusUnsupportedMediaType, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/clients", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
location := res.Header.Get("Location")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.location, location, fmt.Sprintf("%s: expected location %s got %s", tc.desc, tc.location, location))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClient(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
data := toJSON(client)
|
||||
invalidData := toJSON(manager.Client{
|
||||
Type: "foo",
|
||||
Name: client.Name,
|
||||
Payload: client.Payload,
|
||||
})
|
||||
svc.Register(user)
|
||||
id, _ := svc.AddClient(user.Email, client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
id string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"update existing client", data, id, contentType, user.Email, http.StatusOK},
|
||||
{"update non-existent client", data, wrongID, contentType, user.Email, http.StatusNotFound},
|
||||
{"update client with invalid id", data, "1", contentType, user.Email, http.StatusNotFound},
|
||||
{"update client with invalid data", invalidData, id, contentType, user.Email, http.StatusBadRequest},
|
||||
{"update client with invalid user token", data, id, contentType, invalidEmail, http.StatusForbidden},
|
||||
{"update client with invalid data format", "{", id, contentType, user.Email, http.StatusBadRequest},
|
||||
{"update client with empty JSON request", "{}", id, contentType, user.Email, http.StatusBadRequest},
|
||||
{"update client with empty request", "", id, contentType, user.Email, http.StatusBadRequest},
|
||||
{"update client with missing content type", data, id, "", user.Email, http.StatusUnsupportedMediaType},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewClient(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
id, _ := svc.AddClient(user.Email, client)
|
||||
|
||||
client.ID = id
|
||||
client.Key = id
|
||||
data := toJSON(client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
res string
|
||||
}{
|
||||
{"view existing client", id, user.Email, http.StatusOK, data},
|
||||
{"view non-existent client", wrongID, user.Email, http.StatusNotFound, ""},
|
||||
{"view client by passing invalid id", "1", user.Email, http.StatusNotFound, ""},
|
||||
{"view client by passing invalid token", id, invalidEmail, http.StatusForbidden, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
data := strings.Trim(string(body), "\n")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.res, data, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListClients(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
noClientsUser := manager.User{Email: "no_clients_user@example.com", Password: user.Password}
|
||||
svc.Register(noClientsUser)
|
||||
clients := []manager.Client{}
|
||||
for i := 0; i < 101; i++ {
|
||||
id, _ := svc.AddClient(user.Email, client)
|
||||
client.ID = id
|
||||
client.Key = id
|
||||
clients = append(clients, client)
|
||||
}
|
||||
clientURL := fmt.Sprintf("%s/clients", ts.URL)
|
||||
cases := []struct {
|
||||
desc string
|
||||
auth string
|
||||
status int
|
||||
url string
|
||||
res []manager.Client
|
||||
}{
|
||||
{"get a list of clients", user.Email, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 5), clients[0:5]},
|
||||
{"get a list of clients with invalid token", invalidEmail, http.StatusForbidden, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 1), nil},
|
||||
{"get a list of clients with invalid offset", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, -1, 5), nil},
|
||||
{"get a list of clients with invalid limit", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 1, -5), nil},
|
||||
{"get a list of clients with zero limit", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 1, 0), nil},
|
||||
{"get a list of clients with no offset provided", user.Email, http.StatusOK, fmt.Sprintf("%s?limit=%d", clientURL, 5), clients[0:5]},
|
||||
{"get a list of clients with no limit provided", user.Email, http.StatusOK, fmt.Sprintf("%s?offset=%d", clientURL, 1), clients[1:11]},
|
||||
{"get a list of clients with redundant query params", user.Email, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d&value=something", clientURL, 0, 5), clients[0:5]},
|
||||
{"get a list of clients with limit greater than max", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 110), nil},
|
||||
{"get a list of clients with default URL", user.Email, http.StatusOK, fmt.Sprintf("%s%s", clientURL, ""), clients[0:10]},
|
||||
{"get a list of clients with invalid URL", user.Email, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?%%"), nil},
|
||||
{"get a list of clients with invalid number of params", user.Email, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=4&limit=4&limit=5&offset=5"), nil},
|
||||
{"get a list of clients with invalid offset", user.Email, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=e&limit=5"), nil},
|
||||
{"get a list of clients with invalid limit", user.Email, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=5&limit=e"), nil},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: tc.url,
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
var data map[string][]manager.Client
|
||||
json.NewDecoder(res.Body).Decode(&data)
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.ElementsMatch(t, tc.res, data["clients"], fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data["clients"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveClient(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
id, _ := svc.AddClient(user.Email, client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"delete existing client", id, user.Email, http.StatusNoContent},
|
||||
{"delete non-existent client", wrongID, user.Email, http.StatusNoContent},
|
||||
{"delete client with invalid id", "1", user.Email, http.StatusNoContent},
|
||||
{"delete client with invalid token", id, invalidEmail, http.StatusForbidden},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
data := toJSON(channel)
|
||||
svc.Register(user)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
location string
|
||||
}{
|
||||
{"create new channel", data, contentType, user.Email, http.StatusCreated, fmt.Sprintf("/channels/%s", id)},
|
||||
{"create new channel with invalid token", data, contentType, invalidEmail, http.StatusForbidden, ""},
|
||||
{"create new channel with invalid data format", "{", contentType, user.Email, http.StatusBadRequest, ""},
|
||||
{"create new channel with empty JSON request", "{}", contentType, user.Email, http.StatusCreated, "/channels/123e4567-e89b-12d3-a456-000000000002"},
|
||||
{"create new channel with empty request", "", contentType, user.Email, http.StatusBadRequest, ""},
|
||||
{"create new channel with missing content type", data, "", user.Email, http.StatusUnsupportedMediaType, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/channels", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
location := res.Header.Get("Location")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.location, location, fmt.Sprintf("%s: expected location %s got %s", tc.desc, tc.location, location))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
updateData := toJSON(map[string]string{
|
||||
"name": "updated_channel",
|
||||
})
|
||||
svc.Register(user)
|
||||
id, _ := svc.CreateChannel(user.Email, channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
id string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"update existing channel", updateData, id, contentType, user.Email, http.StatusOK},
|
||||
{"update non-existing channel", updateData, wrongID, contentType, user.Email, http.StatusNotFound},
|
||||
{"update channel with invalid token", updateData, id, contentType, invalidEmail, http.StatusForbidden},
|
||||
{"update channel with invalid id", updateData, "1", contentType, user.Email, http.StatusNotFound},
|
||||
{"update channel with invalid data format", "}", id, contentType, user.Email, http.StatusBadRequest},
|
||||
{"update channel with empty JSON object", "{}", id, contentType, user.Email, http.StatusOK},
|
||||
{"update channel with empty request", "", id, contentType, user.Email, http.StatusBadRequest},
|
||||
{"update channel with missing content type", updateData, id, "", user.Email, http.StatusUnsupportedMediaType},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
id, _ := svc.CreateChannel(user.Email, channel)
|
||||
channel.ID = id
|
||||
data := toJSON(channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
res string
|
||||
}{
|
||||
{"view existing channel", id, user.Email, http.StatusOK, data},
|
||||
{"view non-existent channel", wrongID, user.Email, http.StatusNotFound, ""},
|
||||
{"view channel with invalid id", "1", user.Email, http.StatusNotFound, ""},
|
||||
{"view channel with invalid token", id, invalidEmail, http.StatusForbidden, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
body := strings.Trim(string(data), "\n")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.res, body, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListChannels(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
channels := []manager.Channel{}
|
||||
for i := 0; i < 101; i++ {
|
||||
id, _ := svc.CreateChannel(user.Email, channel)
|
||||
channel.ID = id
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
channelURL := fmt.Sprintf("%s/channels", ts.URL)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
auth string
|
||||
status int
|
||||
url string
|
||||
res []manager.Channel
|
||||
}{
|
||||
{"get a list of channels", user.Email, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 6), channels[0:6]},
|
||||
{"get a list of channels with invalid token", invalidEmail, http.StatusForbidden, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 1), nil},
|
||||
{"get a list of channels with invalid offset", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, -1, 5), nil},
|
||||
{"get a list of channels with invalid limit", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, -1, 5), nil},
|
||||
{"get a list of channels with zero limit", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 1, 0), nil},
|
||||
{"get a list of channels with no offset provided", user.Email, http.StatusOK, fmt.Sprintf("%s?limit=%d", channelURL, 5), channels[0:5]},
|
||||
{"get a list of channels with no limit provided", user.Email, http.StatusOK, fmt.Sprintf("%s?offset=%d", channelURL, 1), channels[1:11]},
|
||||
{"get a list of channels with redundant query params", user.Email, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d&value=something", channelURL, 0, 5), channels[0:5]},
|
||||
{"get a list of channels with limit greater than max", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 110), nil},
|
||||
{"get a list of channels with default URL", user.Email, http.StatusOK, fmt.Sprintf("%s%s", channelURL, ""), channels[0:10]},
|
||||
{"get a list of channels with invalid URL", user.Email, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?%%"), nil},
|
||||
{"get a list of channels with invalid number of params", user.Email, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=4&limit=4&limit=5&offset=5"), nil},
|
||||
{"get a list of channels with invalid offset", user.Email, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=e&limit=5"), nil},
|
||||
{"get a list of channels with invalid limit", user.Email, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=5&limit=e"), nil},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: tc.url,
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
var body map[string][]manager.Channel
|
||||
json.NewDecoder(res.Body).Decode(&body)
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.ElementsMatch(t, tc.res, body["channels"], fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, body["channels"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
id, _ := svc.CreateChannel(user.Email, channel)
|
||||
channel.ID = id
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"remove existing channel", channel.ID, user.Email, http.StatusNoContent},
|
||||
{"remove non-existent channel", channel.ID, user.Email, http.StatusNoContent},
|
||||
{"remove channel with invalid id", wrongID, user.Email, http.StatusNoContent},
|
||||
{"remove channel with invalid token", channel.ID, invalidEmail, http.StatusForbidden},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
clientID, _ := svc.AddClient(user.Email, client)
|
||||
chanID, _ := svc.CreateChannel(user.Email, channel)
|
||||
|
||||
otherUser := manager.User{Email: "other_user@example.com", Password: "password"}
|
||||
svc.Register(otherUser)
|
||||
otherClientID, _ := svc.AddClient(otherUser.Email, client)
|
||||
otherChanID, _ := svc.CreateChannel(otherUser.Email, channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chanID string
|
||||
clientID string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"connect existing client to existing channel", chanID, clientID, user.Email, http.StatusOK},
|
||||
{"connect existing client to non-existent channel", wrongID, clientID, user.Email, http.StatusNotFound},
|
||||
{"connect client with invalid id to channel", chanID, "1", user.Email, http.StatusNotFound},
|
||||
{"connect client to channel with invalid id", "1", clientID, user.Email, http.StatusNotFound},
|
||||
{"connect existing client to existing channel with invalid token", chanID, clientID, invalidEmail, http.StatusForbidden},
|
||||
{"connect client from owner to channel of other user", otherChanID, clientID, user.Email, http.StatusNotFound},
|
||||
{"connect client from other user to owner's channel", chanID, otherClientID, user.Email, http.StatusNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/channels/%s/clients/%s", ts.URL, tc.chanID, tc.clientID),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnnect(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
clientID, _ := svc.AddClient(user.Email, client)
|
||||
chanID, _ := svc.CreateChannel(user.Email, channel)
|
||||
svc.Connect(user.Email, chanID, clientID)
|
||||
otherUser := manager.User{Email: "other_user@example.com", Password: "password"}
|
||||
svc.Register(otherUser)
|
||||
otherClientID, _ := svc.AddClient(otherUser.Email, client)
|
||||
otherChanID, _ := svc.CreateChannel(otherUser.Email, channel)
|
||||
svc.Connect(otherUser.Email, otherChanID, otherClientID)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chanID string
|
||||
clientID string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"disconnect connected client from channel", chanID, clientID, user.Email, http.StatusNoContent},
|
||||
{"disconnect non-connected client from channel", chanID, clientID, user.Email, http.StatusNotFound},
|
||||
{"disconnect non-existent client from channel", chanID, "1", user.Email, http.StatusNotFound},
|
||||
{"disconnect client from non-existent channel", "1", clientID, user.Email, http.StatusNotFound},
|
||||
{"disconnect client from channel with invalid token", chanID, clientID, invalidEmail, http.StatusForbidden},
|
||||
{"disconnect owner's client from someone elses channel", otherChanID, clientID, user.Email, http.StatusNotFound},
|
||||
{"disconnect other's client from owner's channel", chanID, otherClientID, user.Email, http.StatusNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/channels/%s/clients/%s", ts.URL, tc.chanID, tc.clientID),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentity(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
clientID, _ := svc.AddClient(user.Email, client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
status int
|
||||
clientID string
|
||||
}{
|
||||
{"get client id using existing client key", clientID, http.StatusOK, clientID},
|
||||
{"get client id using non-existent client key", "", http.StatusForbidden, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/access-grant", ts.URL),
|
||||
token: tc.key,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
clientID := res.Header.Get("X-client-id")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.clientID, clientID, fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.clientID, clientID))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanAccess(t *testing.T) {
|
||||
svc := newService()
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
svc.Register(user)
|
||||
clientID, _ := svc.AddClient(user.Email, client)
|
||||
notConnectedClientID, _ := svc.AddClient(user.Email, client)
|
||||
chanID, _ := svc.CreateChannel(user.Email, channel)
|
||||
svc.Connect(user.Email, chanID, clientID)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chanID string
|
||||
clientKey string
|
||||
status int
|
||||
clientID string
|
||||
}{
|
||||
{"check access to existing channel given connected client", chanID, clientID, http.StatusOK, clientID},
|
||||
{"check access to existing channel given not connected client", chanID, notConnectedClientID, http.StatusForbidden, ""},
|
||||
{"check access to existing channel given non-existent client", chanID, "invalid_token", http.StatusForbidden, ""},
|
||||
{"check access to non-existent channel given existing client", "invalid_token", clientID, http.StatusForbidden, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/channels/%s/access-grant", ts.URL, tc.chanID),
|
||||
token: tc.clientKey,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
clientID := res.Header.Get("X-client-id")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.clientID, clientID, fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.clientID, clientID))
|
||||
}
|
||||
}
|
@ -1,208 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
type apiRes interface {
|
||||
code() int
|
||||
headers() map[string]string
|
||||
empty() bool
|
||||
}
|
||||
|
||||
type identityRes struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (res identityRes) headers() map[string]string {
|
||||
return map[string]string{
|
||||
"X-client-id": res.id,
|
||||
}
|
||||
}
|
||||
|
||||
func (res identityRes) code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res identityRes) empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type tokenRes struct {
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
func (res tokenRes) code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res tokenRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res tokenRes) empty() bool {
|
||||
return res.Token == ""
|
||||
}
|
||||
|
||||
type removeRes struct{}
|
||||
|
||||
func (res removeRes) code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res removeRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res removeRes) empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type clientRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res clientRes) code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res clientRes) headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprint("/clients/", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res clientRes) empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type viewClientRes struct {
|
||||
manager.Client
|
||||
}
|
||||
|
||||
func (res viewClientRes) code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewClientRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewClientRes) empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listClientsRes struct {
|
||||
Clients []manager.Client `json:"clients"`
|
||||
}
|
||||
|
||||
func (res listClientsRes) code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listClientsRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listClientsRes) empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res channelRes) code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res channelRes) headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprint("/channels/", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res channelRes) empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type viewChannelRes struct {
|
||||
manager.Channel
|
||||
}
|
||||
|
||||
func (res viewChannelRes) code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewChannelRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewChannelRes) empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listChannelsRes struct {
|
||||
Channels []manager.Channel `json:"channels"`
|
||||
}
|
||||
|
||||
func (res listChannelsRes) code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listChannelsRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listChannelsRes) empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type connectionRes struct{}
|
||||
|
||||
func (res connectionRes) code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res connectionRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res connectionRes) empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type disconnectionRes struct{}
|
||||
|
||||
func (res disconnectionRes) code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res disconnectionRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res disconnectionRes) empty() bool {
|
||||
return true
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
// Package bcrypt provides a hasher implementation utilising bcrypt.
|
||||
package bcrypt
|
@ -1,104 +0,0 @@
|
||||
// Package client provides a manager service client intended for internal
|
||||
// service communication.
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/sony/gobreaker"
|
||||
)
|
||||
|
||||
const (
|
||||
timeout = time.Second * 5
|
||||
maxFailedReqs = 3
|
||||
maxFailureRatio = 0.6
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrServiceUnreachable indicates that the service instance is not available.
|
||||
ErrServiceUnreachable = errors.New("manager service unavailable")
|
||||
|
||||
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
|
||||
// when accessing a protected resource.
|
||||
ErrUnauthorizedAccess = manager.ErrUnauthorizedAccess
|
||||
)
|
||||
|
||||
// ManagerClient provides an access to the manager service authorization
|
||||
// endpoints.
|
||||
type ManagerClient struct {
|
||||
url string
|
||||
cb *gobreaker.CircuitBreaker
|
||||
}
|
||||
|
||||
// NewClient instantiates the manager service client given its base URL.
|
||||
func NewClient(url string) ManagerClient {
|
||||
st := gobreaker.Settings{
|
||||
Name: "Manager",
|
||||
ReadyToTrip: func(counts gobreaker.Counts) bool {
|
||||
fr := float64(counts.TotalFailures) / float64(counts.Requests)
|
||||
return counts.Requests >= maxFailedReqs && fr >= maxFailureRatio
|
||||
},
|
||||
}
|
||||
|
||||
mc := ManagerClient{
|
||||
url: url,
|
||||
cb: gobreaker.NewCircuitBreaker(st),
|
||||
}
|
||||
|
||||
return mc
|
||||
}
|
||||
|
||||
// VerifyToken tries to extract an identity from the provided token.
|
||||
func (mc ManagerClient) VerifyToken(token string) (string, error) {
|
||||
url := fmt.Sprintf("%s/access-grant", mc.url)
|
||||
return mc.makeRequest(url, token)
|
||||
}
|
||||
|
||||
// CanAccess checks whether or not the client having a provided token has
|
||||
// access to the specified channel.
|
||||
func (mc ManagerClient) CanAccess(channel, token string) (string, error) {
|
||||
url := fmt.Sprintf("%s/channels/%s/access-grant", mc.url, channel)
|
||||
return mc.makeRequest(url, token)
|
||||
}
|
||||
|
||||
func (mc ManagerClient) makeRequest(url, token string) (string, error) {
|
||||
response, err := mc.cb.Execute(func() (interface{}, error) {
|
||||
hc := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
mgReq, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", ErrServiceUnreachable
|
||||
}
|
||||
|
||||
mgReq.Header.Set("Authorization", token)
|
||||
|
||||
res, err := hc.Do(mgReq)
|
||||
if err != nil {
|
||||
return "", ErrServiceUnreachable
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return ErrUnauthorizedAccess, nil
|
||||
}
|
||||
|
||||
return res.Header.Get("X-client-id"), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id, ok := response.(string)
|
||||
if !ok {
|
||||
return "", manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
// Package manager contains the domain concept definitions needed to support
|
||||
// Mainflux manager service functionality.
|
||||
package manager
|
@ -1,2 +0,0 @@
|
||||
// Package jwt provides a JWT identity provider.
|
||||
package jwt
|
@ -1,231 +0,0 @@
|
||||
package manager
|
||||
|
||||
var _ Service = (*managerService)(nil)
|
||||
|
||||
type managerService struct {
|
||||
users UserRepository
|
||||
clients ClientRepository
|
||||
channels ChannelRepository
|
||||
hasher Hasher
|
||||
idp IdentityProvider
|
||||
}
|
||||
|
||||
// New instantiates the domain service implementation.
|
||||
func New(users UserRepository, clients ClientRepository, channels ChannelRepository, hasher Hasher, idp IdentityProvider) Service {
|
||||
return &managerService{
|
||||
users: users,
|
||||
clients: clients,
|
||||
channels: channels,
|
||||
hasher: hasher,
|
||||
idp: idp,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *managerService) Register(user User) error {
|
||||
hash, err := ms.hasher.Hash(user.Password)
|
||||
if err != nil {
|
||||
return ErrMalformedEntity
|
||||
}
|
||||
|
||||
user.Password = hash
|
||||
return ms.users.Save(user)
|
||||
}
|
||||
|
||||
func (ms *managerService) Login(user User) (string, error) {
|
||||
dbUser, err := ms.users.One(user.Email)
|
||||
if err != nil {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if err := ms.hasher.Compare(user.Password, dbUser.Password); err != nil {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.idp.TemporaryKey(user.Email)
|
||||
}
|
||||
|
||||
func (ms *managerService) AddClient(key string, client Client) (string, error) {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
client.ID = ms.clients.Id()
|
||||
client.Owner = sub
|
||||
client.Key, _ = ms.idp.PermanentKey(client.ID)
|
||||
|
||||
return client.ID, ms.clients.Save(client)
|
||||
}
|
||||
|
||||
func (ms *managerService) UpdateClient(key string, client Client) error {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
client.Owner = sub
|
||||
|
||||
return ms.clients.Update(client)
|
||||
}
|
||||
|
||||
func (ms *managerService) ViewClient(key, id string) (Client, error) {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return Client{}, err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return Client{}, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.clients.One(sub, id)
|
||||
}
|
||||
|
||||
func (ms *managerService) ListClients(key string, offset, limit int) ([]Client, error) {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return nil, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.clients.All(sub, offset, limit), nil
|
||||
}
|
||||
|
||||
func (ms *managerService) RemoveClient(key, id string) error {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.clients.Remove(sub, id)
|
||||
}
|
||||
|
||||
func (ms *managerService) CreateChannel(key string, channel Channel) (string, error) {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
channel.Owner = sub
|
||||
return ms.channels.Save(channel)
|
||||
}
|
||||
|
||||
func (ms *managerService) UpdateChannel(key string, channel Channel) error {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
channel.Owner = sub
|
||||
return ms.channels.Update(channel)
|
||||
}
|
||||
|
||||
func (ms *managerService) ViewChannel(key, id string) (Channel, error) {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return Channel{}, err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return Channel{}, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.One(sub, id)
|
||||
}
|
||||
|
||||
func (ms *managerService) ListChannels(key string, offset, limit int) ([]Channel, error) {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return nil, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.All(sub, offset, limit), nil
|
||||
}
|
||||
|
||||
func (ms *managerService) RemoveChannel(key, id string) error {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.Remove(sub, id)
|
||||
}
|
||||
|
||||
func (ms *managerService) Connect(key, chanId, clientId string) error {
|
||||
owner, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(owner); err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.Connect(owner, chanId, clientId)
|
||||
}
|
||||
|
||||
func (ms *managerService) Disconnect(key, chanId, clientId string) error {
|
||||
owner, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(owner); err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.Disconnect(owner, chanId, clientId)
|
||||
}
|
||||
|
||||
func (ms *managerService) Identity(key string) (string, error) {
|
||||
client, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (ms *managerService) CanAccess(key, channel string) (string, error) {
|
||||
client, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !ms.channels.HasClient(channel, client) {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
@ -1,421 +0,0 @@
|
||||
package manager_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const wrong string = "wrong-value"
|
||||
|
||||
var (
|
||||
user manager.User = manager.User{"user@example.com", "password"}
|
||||
client manager.Client = manager.Client{Type: "app", Name: "test"}
|
||||
channel manager.Channel = manager.Channel{Name: "test", Clients: []manager.Client{}}
|
||||
)
|
||||
|
||||
func newService() manager.Service {
|
||||
users := mocks.NewUserRepository()
|
||||
clients := mocks.NewClientRepository()
|
||||
channels := mocks.NewChannelRepository(clients)
|
||||
hasher := mocks.NewHasher()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
|
||||
return manager.New(users, clients, channels, hasher, idp)
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
svc := newService()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
user manager.User
|
||||
err error
|
||||
}{
|
||||
{"register new user", user, nil},
|
||||
{"register existing user", user, manager.ErrConflict},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Register(tc.user)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
user manager.User
|
||||
err error
|
||||
}{
|
||||
"login with good credentials": {user, nil},
|
||||
"login with wrong e-mail": {manager.User{wrong, user.Password}, manager.ErrUnauthorizedAccess},
|
||||
"login with wrong password": {manager.User{user.Email, wrong}, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.Login(tc.user)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddClient(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"add new app": {manager.Client{Type: "app", Name: "a"}, key, nil},
|
||||
"add new device": {manager.Client{Type: "device", Name: "b"}, key, nil},
|
||||
"add client with wrong credentials": {manager.Client{Type: "app", Name: "d"}, wrong, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.AddClient(tc.key, tc.client)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClient(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
client.ID = clientId
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"update existing client": {client, key, nil},
|
||||
"update client with wrong credentials": {client, wrong, manager.ErrUnauthorizedAccess},
|
||||
"update non-existing client": {manager.Client{ID: "2", Type: "app", Name: "d"}, key, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.UpdateClient(tc.key, tc.client)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewClient(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
client.ID = clientId
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"view existing client": {client.ID, key, nil},
|
||||
"view client with wrong credentials": {client.ID, wrong, manager.ErrUnauthorizedAccess},
|
||||
"view non-existing client": {wrong, key, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ViewClient(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListClients(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
n := 10
|
||||
for i := 0; i < n; i++ {
|
||||
svc.AddClient(key, client)
|
||||
}
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
offset int
|
||||
limit int
|
||||
size int
|
||||
err error
|
||||
}{
|
||||
"list clients": {key, 0, 5, 5, nil},
|
||||
"list clients 5-10": {key, 5, 10, 5, nil},
|
||||
"list last client": {key, 9, 10, 1, nil},
|
||||
"list empty response": {key, 11, 10, 0, nil},
|
||||
"list offset < 0": {key, -1, 10, 0, nil},
|
||||
"list limit < 0": {key, 1, -10, 0, nil},
|
||||
"list limit = 0": {key, 1, 0, 0, nil},
|
||||
"list clients with wrong credentials": {wrong, 0, 0, 0, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
cl, err := svc.ListClients(tc.key, tc.offset, tc.limit)
|
||||
size := len(cl)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveClient(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
client.ID = clientId
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"remove client with wrong credentials": {client.ID, "?", manager.ErrUnauthorizedAccess},
|
||||
"remove existing client": {client.ID, key, nil},
|
||||
"remove removed client": {client.ID, key, nil},
|
||||
"remove non-existing client": {"?", key, nil},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.RemoveClient(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"create channel": {manager.Channel{}, key, nil},
|
||||
"create channel with wrong credentials": {manager.Channel{}, wrong, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.CreateChannel(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
channel.ID = chanId
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"update existing channel": {channel, key, nil},
|
||||
"update channel with wrong credentials": {channel, wrong, manager.ErrUnauthorizedAccess},
|
||||
"update non-existing channel": {manager.Channel{ID: "2", Name: "test"}, key, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.UpdateChannel(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
channel.ID = chanId
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"view existing channel": {channel.ID, key, nil},
|
||||
"view channel with wrong credentials": {channel.ID, wrong, manager.ErrUnauthorizedAccess},
|
||||
"view non-existing channel": {wrong, key, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ViewChannel(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListChannels(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
n := 10
|
||||
for i := 0; i < n; i++ {
|
||||
svc.CreateChannel(key, channel)
|
||||
}
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
offset int
|
||||
limit int
|
||||
size int
|
||||
err error
|
||||
}{
|
||||
"list first 5 channels": {key, 0, 5, 5, nil},
|
||||
"list channels 5-10 channels": {key, 5, 10, 5, nil},
|
||||
"list last channel": {key, 6, 10, 4, nil},
|
||||
"list offset < 0": {key, -1, 10, 0, nil},
|
||||
"list limit < 0": {key, 1, -10, 0, nil},
|
||||
"list limit = 0": {key, 1, 0, 0, nil},
|
||||
"list channels with wrong credentials": {wrong, 0, 0, 0, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
ch, err := svc.ListChannels(tc.key, tc.offset, tc.limit)
|
||||
size := len(ch)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
channel.ID = chanId
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"remove channel with wrong credentials": {channel.ID, wrong, manager.ErrUnauthorizedAccess},
|
||||
"remove existing channel": {channel.ID, key, nil},
|
||||
"remove removed channel": {channel.ID, key, nil},
|
||||
"remove non-existing channel": {channel.ID, key, nil},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.RemoveChannel(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
client.ID = clientId
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
channel.ID = chanId
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
chanId string
|
||||
clientId string
|
||||
err error
|
||||
}{
|
||||
"connect client": {key, channel.ID, client.ID, nil},
|
||||
"connect client with wrong credentials": {wrong, channel.ID, client.ID, manager.ErrUnauthorizedAccess},
|
||||
"connect client to non-existing channel": {key, wrong, client.ID, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.Connect(tc.key, tc.chanId, tc.clientId)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnect(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
client.ID = clientId
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
channel.ID = chanId
|
||||
|
||||
svc.Connect(key, chanId, clientId)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
chanId string
|
||||
clientId string
|
||||
err error
|
||||
}{
|
||||
{"disconnect connected client", key, channel.ID, client.ID, nil},
|
||||
{"disconnect disconnected client", key, channel.ID, client.ID, manager.ErrNotFound},
|
||||
{"disconnect client with wrong credentials", wrong, channel.ID, client.ID, manager.ErrUnauthorizedAccess},
|
||||
{"disconnect client from non-existing channel", key, wrong, client.ID, manager.ErrNotFound},
|
||||
{"disconnect non-existing client", key, channel.ID, wrong, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Disconnect(tc.key, tc.chanId, tc.clientId)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIdentity(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid token's identity": {key, nil},
|
||||
"invalid token's identity": {"", manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.Identity(tc.key)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanAccess(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
client.ID = clientId
|
||||
client.Key = clientId
|
||||
|
||||
channel.Clients = []manager.Client{client}
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
channel.ID = chanId
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
channel string
|
||||
err error
|
||||
}{
|
||||
"allowed access": {client.Key, channel.ID, nil},
|
||||
"not-connected cannot access": {wrong, channel.ID, manager.ErrUnauthorizedAccess},
|
||||
"access non-existing channel": {client.Key, wrong, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.CanAccess(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
var _ manager.UserRepository = (*userRepositoryMock)(nil)
|
||||
|
||||
type userRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
users map[string]manager.User
|
||||
}
|
||||
|
||||
// NewUserRepository creates in-memory user repository.
|
||||
func NewUserRepository() manager.UserRepository {
|
||||
return &userRepositoryMock{
|
||||
users: make(map[string]manager.User),
|
||||
}
|
||||
}
|
||||
|
||||
func (urm *userRepositoryMock) Save(user manager.User) error {
|
||||
urm.mu.Lock()
|
||||
defer urm.mu.Unlock()
|
||||
|
||||
if _, ok := urm.users[user.Email]; ok {
|
||||
return manager.ErrConflict
|
||||
}
|
||||
|
||||
urm.users[user.Email] = user
|
||||
return nil
|
||||
}
|
||||
|
||||
func (urm *userRepositoryMock) One(email string) (manager.User, error) {
|
||||
urm.mu.Lock()
|
||||
defer urm.mu.Unlock()
|
||||
|
||||
if val, ok := urm.users[email]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
return manager.User{}, manager.ErrUnauthorizedAccess
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserSave(t *testing.T) {
|
||||
email := "user-save@example.com"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
user manager.User
|
||||
err error
|
||||
}{
|
||||
{"new user", manager.User{email, "pass"}, nil},
|
||||
{"duplicate user", manager.User{email, "pass"}, manager.ErrConflict},
|
||||
}
|
||||
|
||||
repo := postgres.NewUserRepository(db)
|
||||
|
||||
for _, tc := range cases {
|
||||
err := repo.Save(tc.user)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleUserRetrieval(t *testing.T) {
|
||||
email := "user-retrieval@example.com"
|
||||
|
||||
repo := postgres.NewUserRepository(db)
|
||||
repo.Save(manager.User{email, "pass"})
|
||||
|
||||
cases := map[string]struct {
|
||||
email string
|
||||
err error
|
||||
}{
|
||||
"existing user": {email, nil},
|
||||
"non-existing user": {"unknown@example.com", manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := repo.One(tc.email)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ locally:
|
||||
```yaml
|
||||
version: "2"
|
||||
services:
|
||||
manager:
|
||||
normalizer:
|
||||
image: mainflux/normalizer:[version]
|
||||
container_name: [instance name]
|
||||
environment:
|
||||
|
78
users/README.md
Normal file
78
users/README.md
Normal file
@ -0,0 +1,78 @@
|
||||
# Users service
|
||||
|
||||
Users service provides an HTTP API for managing users. Through this API clients
|
||||
are able to do the following actions:
|
||||
|
||||
- register new accounts
|
||||
- obtain access tokens
|
||||
- verify access tokens
|
||||
|
||||
For in-depth explanation of the aforementioned scenarios, as well as thorough
|
||||
understanding of Mainflux, please check out the [official documentation][doc].
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the
|
||||
following table. Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------------------|------------------------------------------|--------------|
|
||||
| MF_USERS_DB_HOST | Database host address | localhost |
|
||||
| MF_USERS_DB_PORT | Database host port | 5432 |
|
||||
| MF_USERS_DB_USER | Database user | mainflux |
|
||||
| MF_USERS_DB_PASSWORD | Database password | mainflux |
|
||||
| MF_USERS_DB | Name of the database used by the service | users |
|
||||
| MF_USERS_HTTP_PORT | Users service HTTP port | 8180 |
|
||||
| MF_USERS_GRPC_PORT | Users service gRPC port | 8181 |
|
||||
| MF_USERS_SECRET | String used for signing tokens | users |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. The following snippet
|
||||
provides a compose file template that can be used to deploy the service container
|
||||
locally:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
services:
|
||||
users:
|
||||
image: mainflux/users:[version]
|
||||
container_name: [instance name]
|
||||
ports:
|
||||
- [host machine port]:[configured HTTP port]
|
||||
environment:
|
||||
MF_USERS_DB_HOST: [Database host address]
|
||||
MF_USERS_DB_PORT: [Database host port]
|
||||
MF_USERS_DB_USER: [Database user]
|
||||
MF_USERS_DB_PASS: [Database password]
|
||||
MF_USERS_DB: [Name of the database used by the service]
|
||||
MF_USERS_HTTP_PORT: [Service HTTP port]
|
||||
MF_USERS_GRPC_PORT: [Service gRPC port]
|
||||
MF_USERS_SECRET: [String used for signing tokens]
|
||||
```
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
go get github.com/mainflux/mainflux
|
||||
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux
|
||||
|
||||
# compile the app
|
||||
make users
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_USERS_DB_HOST=[Database host address] MF_USERS_DB_PORT=[Database host port] MF_USERS_DB_USER=[Database user] MF_USERS_DB_PASS=[Database password] MF_USERS_DB=[Name of the database used by the service] MF_USERS_HTTP_PORT=[Service HTTP port] MF_USERS_GRPC_PORT=[Service gRPC port] MF_USERS_SECRET=[String used for signing tokens] $GOBIN/mainflux-users
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](swagger.yaml).
|
||||
|
||||
[doc]: http://mainflux.readthedocs.io
|
3
users/api/doc.go
Normal file
3
users/api/doc.go
Normal file
@ -0,0 +1,3 @@
|
||||
// Package api contains API-related concerns: endpoint definitions, middlewares
|
||||
// and all resource representations.
|
||||
package api
|
49
users/api/grpc/client.go
Normal file
49
users/api/grpc/client.go
Normal file
@ -0,0 +1,49 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"github.com/mainflux/mainflux"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.UsersServiceClient = (*grpcClient)(nil)
|
||||
|
||||
type grpcClient struct {
|
||||
identify endpoint.Endpoint
|
||||
}
|
||||
|
||||
// NewClient returns new gRPC client instance.
|
||||
func NewClient(conn *grpc.ClientConn) mainflux.UsersServiceClient {
|
||||
endpoint := kitgrpc.NewClient(
|
||||
conn,
|
||||
"mainflux.UsersService",
|
||||
"Identify",
|
||||
encodeIdentifyRequest,
|
||||
decodeIdentifyResponse,
|
||||
mainflux.Identity{},
|
||||
).Endpoint()
|
||||
|
||||
return &grpcClient{endpoint}
|
||||
}
|
||||
|
||||
func (client grpcClient) Identify(ctx context.Context, token *mainflux.Token, _ ...grpc.CallOption) (*mainflux.Identity, error) {
|
||||
res, err := client.identify(ctx, identityReq{token.GetValue()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ir := res.(identityRes)
|
||||
return &mainflux.Identity{Value: ir.id}, ir.err
|
||||
}
|
||||
|
||||
func encodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(identityReq)
|
||||
return &mainflux.Token{Value: req.token}, nil
|
||||
}
|
||||
|
||||
func decodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.Identity)
|
||||
return identityRes{res.GetValue(), nil}, nil
|
||||
}
|
2
users/api/grpc/doc.go
Normal file
2
users/api/grpc/doc.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package grpc contains implementation of users service gRPC API.
|
||||
package grpc
|
22
users/api/grpc/endpoint.go
Normal file
22
users/api/grpc/endpoint.go
Normal file
@ -0,0 +1,22 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
context "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func identifyEndpoint(svc users.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(identityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := svc.Identify(req.token)
|
||||
if err != nil {
|
||||
return identityRes{}, err
|
||||
}
|
||||
return identityRes{id, nil}, nil
|
||||
}
|
||||
}
|
64
users/api/grpc/endpoint_test.go
Normal file
64
users/api/grpc/endpoint_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
grpcapi "github.com/mainflux/mainflux/users/api/grpc"
|
||||
"github.com/mainflux/mainflux/users/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const port = 8080
|
||||
|
||||
var user = users.User{"john.doe@email.com", "pass"}
|
||||
|
||||
func newService() users.Service {
|
||||
repo := mocks.NewUserRepository()
|
||||
hasher := mocks.NewHasher()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
|
||||
return users.New(repo, hasher, idp)
|
||||
}
|
||||
|
||||
func startGRPCServer(svc users.Service, port int) {
|
||||
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
server := grpc.NewServer()
|
||||
mainflux.RegisterUsersServiceServer(server, grpcapi.NewServer(svc))
|
||||
go server.Serve(listener)
|
||||
}
|
||||
|
||||
func TestIdentify(t *testing.T) {
|
||||
svc := newService()
|
||||
startGRPCServer(svc, port)
|
||||
svc.Register(user)
|
||||
|
||||
usersAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(usersAddr, grpc.WithInsecure())
|
||||
client := grpcapi.NewClient(conn)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
cases := map[string]struct {
|
||||
token string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
"identify user with valid token": {user.Email, user.Email, nil},
|
||||
"identify user that doesn't exist": {"", "", status.Error(codes.InvalidArgument, "received invalid token request")},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
id, err := client.Identify(ctx, &mainflux.Token{tc.token})
|
||||
assert.Equal(t, tc.id, id.GetValue(), fmt.Sprintf("%s: expected %s got %s", desc, tc.id, id.GetValue()))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err))
|
||||
}
|
||||
}
|
16
users/api/grpc/requests.go
Normal file
16
users/api/grpc/requests.go
Normal file
@ -0,0 +1,16 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/users"
|
||||
)
|
||||
|
||||
type identityReq struct {
|
||||
token string
|
||||
}
|
||||
|
||||
func (req identityReq) validate() error {
|
||||
if req.token == "" {
|
||||
return users.ErrMalformedEntity
|
||||
}
|
||||
return nil
|
||||
}
|
6
users/api/grpc/responses.go
Normal file
6
users/api/grpc/responses.go
Normal file
@ -0,0 +1,6 @@
|
||||
package grpc
|
||||
|
||||
type identityRes struct {
|
||||
id string
|
||||
err error
|
||||
}
|
59
users/api/grpc/server.go
Normal file
59
users/api/grpc/server.go
Normal file
@ -0,0 +1,59 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
mainflux "github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var _ mainflux.UsersServiceServer = (*grpcServer)(nil)
|
||||
|
||||
type grpcServer struct {
|
||||
handler kitgrpc.Handler
|
||||
}
|
||||
|
||||
// NewServer returns new UsersServiceServer instance.
|
||||
func NewServer(svc users.Service) mainflux.UsersServiceServer {
|
||||
handler := kitgrpc.NewServer(
|
||||
identifyEndpoint(svc),
|
||||
decodeIdentifyRequest,
|
||||
encodeIdentifyResponse,
|
||||
)
|
||||
return &grpcServer{handler}
|
||||
}
|
||||
|
||||
func (s *grpcServer) Identify(ctx context.Context, token *mainflux.Token) (*mainflux.Identity, error) {
|
||||
_, res, err := s.handler.ServeGRPC(ctx, token)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.Identity), nil
|
||||
}
|
||||
|
||||
func decodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.Token)
|
||||
return identityReq{req.GetValue()}, nil
|
||||
}
|
||||
|
||||
func encodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(identityRes)
|
||||
return &mainflux.Identity{Value: res.id}, encodeError(res.err)
|
||||
}
|
||||
|
||||
func encodeError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch err {
|
||||
case users.ErrMalformedEntity:
|
||||
return status.Error(codes.InvalidArgument, "received invalid token request")
|
||||
case users.ErrUnauthorizedAccess:
|
||||
return status.Error(codes.PermissionDenied, "failed to identify user from token")
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
}
|
2
users/api/http/doc.go
Normal file
2
users/api/http/doc.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package http contains implementation of users service HTTP API.
|
||||
package http
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user