1
0
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:
Aleksandar Novaković 2018-05-10 23:53:25 +02:00 committed by Dejan Mijić
parent 6a361209c8
commit 816c172823
346 changed files with 173191 additions and 3134 deletions

104
Gopkg.lock generated
View File

@ -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

View File

@ -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
View 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
View 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

View 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
View File

@ -0,0 +1,2 @@
// Package grpc contains implementation of clients service gRPC API.
package grpc

View 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
}
}

View 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))
}
}

View 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
}

View File

@ -0,0 +1,6 @@
package grpc
type accessRes struct {
id string
err error
}

View 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
View File

@ -0,0 +1,2 @@
// Package http contains implementation of clients service HTTP API.
package http

View File

@ -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
}
}

View 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))
}
}

View File

@ -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

View File

@ -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 {

View 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
}

View File

@ -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)

View File

@ -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))

View File

@ -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
View 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))
}

View File

@ -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.

View File

@ -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
View 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
View 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
View File

@ -0,0 +1,3 @@
// Package clients contains the domain concept definitions needed to support
// Mainflux clients service functionality.
package clients

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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
View 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
}

View File

@ -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 {

View File

@ -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))
}
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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").

View File

@ -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"
)

View File

@ -1,4 +1,4 @@
package manager
package clients
// Hasher specifies an API for generating hashes of an arbitrary textual
// content.

View File

@ -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)
}

View File

@ -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
View 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)
}

View File

@ -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() {

View File

@ -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() {

View File

@ -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
View 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)
}

View File

@ -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() {

View File

@ -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

View File

@ -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,
}

View File

@ -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

View File

@ -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 |
![arch](img/architecture.jpg)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View 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>

View File

View File

@ -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

View File

@ -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()

View File

@ -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
View 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
View 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;
}

View File

@ -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

View File

@ -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

View 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
View 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

View File

@ -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

View File

@ -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

View 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
View 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

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -21,7 +21,6 @@ kind: ReplicationController
metadata:
name: mainflux-nginx
spec:
replicas: 1
template:
metadata:
labels:

View File

@ -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

View File

@ -1,10 +0,0 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mainflux-postgres-data-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi

View File

@ -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

View File

@ -1,11 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: mainflux-postgres
labels:
name: mainflux-postgres
spec:
ports:
- port: 5432
selector:
name: mainflux-postgres

View File

@ -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

View File

@ -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))
}
}

View File

@ -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
}

View File

@ -1,2 +0,0 @@
// Package bcrypt provides a hasher implementation utilising bcrypt.
package bcrypt

View File

@ -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
}

View File

@ -1,3 +0,0 @@
// Package manager contains the domain concept definitions needed to support
// Mainflux manager service functionality.
package manager

View File

@ -1,2 +0,0 @@
// Package jwt provides a JWT identity provider.
package jwt

View File

@ -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
}

View File

@ -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))
}
}

View File

@ -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
}

View File

@ -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))
}
}

View File

@ -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
View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
// Package grpc contains implementation of users service gRPC API.
package grpc

View 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
}
}

View 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))
}
}

View 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
}

View File

@ -0,0 +1,6 @@
package grpc
type identityRes struct {
id string
err error
}

59
users/api/grpc/server.go Normal file
View 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
View 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