mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 15:25:48 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b393ad50f | |||
| 9d7f4544c7 | |||
| 781d358f38 | |||
| 7b6030d8ad | |||
| 89e69d132a | |||
| 7590578745 | |||
| 97c2d9c30a | |||
| c38a032fa5 | |||
| 1d75268ffa | |||
| ef3627f4ee | |||
| 808ac50792 | |||
| 4c8aeb395c | |||
| 64e71edf95 | |||
| c5a40aeffa | |||
| 3d134d52a6 | |||
| f0cbc47ee6 | |||
| e0e2b2aad0 | |||
| bbf2d5560c | |||
| 69bfb8668e | |||
| 7043dea81a | |||
| e04d0aeaff | |||
| 9c6904d9b3 | |||
| 38a107919a | |||
| 48484210c2 | |||
| ceb630c782 | |||
| 570ebc1c72 |
Generated
+49
-13
@@ -58,12 +58,6 @@
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dereulenspiegel/coap-mux"
|
||||
packages = ["."]
|
||||
revision = "e137e310a99955be01d60b2237da89153ea40987"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
@@ -82,12 +76,6 @@
|
||||
revision = "47565b4f722fb6ceae66b95f853feed578a4a51c"
|
||||
version = "v0.3.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-coap"
|
||||
packages = ["."]
|
||||
revision = "ddcc80675fa42611359d91a6dfa5aa57fb90e72b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-kit/kit"
|
||||
packages = [
|
||||
@@ -144,6 +132,16 @@
|
||||
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/influxdb"
|
||||
packages = [
|
||||
"client/v2",
|
||||
"models",
|
||||
"pkg/escape"
|
||||
]
|
||||
revision = "02d7d4f043b34ecb4e9b2dbec298c6f9450c2a32"
|
||||
version = "v1.5.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/logfmt"
|
||||
@@ -165,6 +163,37 @@
|
||||
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mongodb/mongo-go-driver"
|
||||
packages = [
|
||||
"bson",
|
||||
"bson/builder",
|
||||
"bson/decimal",
|
||||
"bson/elements",
|
||||
"bson/objectid",
|
||||
"core/addr",
|
||||
"core/auth",
|
||||
"core/auth/internal/gssapi",
|
||||
"core/command",
|
||||
"core/connection",
|
||||
"core/connstring",
|
||||
"core/description",
|
||||
"core/dispatch",
|
||||
"core/options",
|
||||
"core/readconcern",
|
||||
"core/readpref",
|
||||
"core/result",
|
||||
"core/tag",
|
||||
"core/topology",
|
||||
"core/version",
|
||||
"core/wiremessage",
|
||||
"core/writeconcern",
|
||||
"internal",
|
||||
"mongo"
|
||||
]
|
||||
revision = "29a4d7d5b648e5b0b60e03e3bd71c11703f0c203"
|
||||
version = "v0.0.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/nats-io/go-nats"
|
||||
packages = [
|
||||
@@ -329,6 +358,7 @@
|
||||
packages = [
|
||||
"bcrypt",
|
||||
"blowfish",
|
||||
"pbkdf2",
|
||||
"ssh/terminal"
|
||||
]
|
||||
revision = "2d027ae1dddd4694d54f7a8b6cbe78dca8720226"
|
||||
@@ -347,6 +377,12 @@
|
||||
]
|
||||
revision = "f73e4c9ed3b7ebdd5f699a16a880c2b1994e50dd"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sync"
|
||||
packages = ["semaphore"]
|
||||
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
@@ -429,6 +465,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "cce4c6cd08b6213615afc135d8e7d4f255d4ee32b42062e1f3208de246366418"
|
||||
inputs-digest = "6010ae4c4f91c30b3a63789cc280466ce56d50f387c1e4d11c49df1302f083da"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
+8
-12
@@ -6,18 +6,10 @@
|
||||
branch = "master"
|
||||
name = "github.com/cisco/senml"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dereulenspiegel/coap-mux"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
version = "3.2.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-coap"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-kit/kit"
|
||||
version = "0.6.0"
|
||||
@@ -30,10 +22,6 @@
|
||||
name = "github.com/golang/protobuf"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/jinzhu/gorm"
|
||||
version = "1.9.1"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/lib/pq"
|
||||
@@ -74,6 +62,14 @@
|
||||
branch = "master"
|
||||
name = "github.com/rubenv/sql-migrate"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/influxdata/influxdb"
|
||||
version = "1.5.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mongodb/mongo-go-driver"
|
||||
version = "0.0.5"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
BUILD_DIR = build
|
||||
SERVICES = users clients http normalizer ws coap
|
||||
SERVICES = users things http normalizer ws influxdb mongodb
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
GOOS ?= linux
|
||||
@@ -33,7 +33,7 @@ $(DOCKERS):
|
||||
|
||||
dockers: $(DOCKERS)
|
||||
docker build --tag=mainflux/dashflux -f dashflux/docker/Dockerfile dashflux
|
||||
docker build --tag=mainflux/mqtt -f mqtt/Dockerfile mqtt
|
||||
docker build --tag=mainflux/mqtt -f mqtt/Dockerfile .
|
||||
|
||||
latest: dockers
|
||||
for svc in $(SERVICES); do \
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# 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
|
||||
@@ -1,49 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Package grpc contains implementation of clients service gRPC API.
|
||||
package grpc
|
||||
@@ -1,22 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
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"
|
||||
wrong = "wrong"
|
||||
email = "john.doe@email.com"
|
||||
)
|
||||
|
||||
var (
|
||||
client = clients.Client{Type: "app", Name: "test_app", Payload: "test_payload"}
|
||||
channel = clients.Channel{Name: "test"}
|
||||
)
|
||||
|
||||
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
|
||||
code codes.Code
|
||||
}{
|
||||
"check if connected client can access existing channel": {connectedClient.Key, chanID, connectedClientID, codes.OK},
|
||||
"check if unconnected client can access existing channel": {client.Key, chanID, "", codes.PermissionDenied},
|
||||
"check if wrong client can access existing channel": {wrong, chanID, "", codes.PermissionDenied},
|
||||
"check if connected client can access non-existent channel": {connectedClient.Key, "1", "", codes.InvalidArgument},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
id, err := cli.CanAccess(ctx, &mainflux.AccessReq{tc.clientKey, tc.chanID})
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "OK expected to be true")
|
||||
assert.Equal(t, tc.id, id.GetValue(), fmt.Sprintf("%s: expected %s got %s", desc, tc.id, id.GetValue()))
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package grpc
|
||||
|
||||
type accessRes struct {
|
||||
id string
|
||||
err error
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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, "missing or invalid credentials provided")
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Package http contains implementation of clients service HTTP API.
|
||||
package http
|
||||
@@ -1,610 +0,0 @@
|
||||
package http_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
httpapi "github.com/mainflux/mainflux/clients/api/http"
|
||||
"github.com/mainflux/mainflux/clients/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
invalidEmail = "userexample.com"
|
||||
email = "user@example.com"
|
||||
token = "token"
|
||||
invalidToken = "invalid_token"
|
||||
wrongID = "123e4567-e89b-12d3-a456-000000000042"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
)
|
||||
|
||||
var (
|
||||
client = clients.Client{Type: "app", Name: "test_app", Payload: "test_payload"}
|
||||
channel = clients.Channel{Name: "test"}
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService(tokens map[string]string) clients.Service {
|
||||
users := mocks.NewUsersService(tokens)
|
||||
clientsRepo := mocks.NewClientRepository()
|
||||
channelsRepo := mocks.NewChannelRepository(clientsRepo)
|
||||
hasher := mocks.NewHasher()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
|
||||
return clients.New(users, clientsRepo, channelsRepo, hasher, idp)
|
||||
}
|
||||
|
||||
func newServer(svc clients.Service) *httptest.Server {
|
||||
mux := httpapi.MakeHandler(svc)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func TestAddClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
data := toJSON(client)
|
||||
invalidData := toJSON(clients.Client{
|
||||
Type: "foo",
|
||||
Name: "invalid_client",
|
||||
Payload: "some_payload",
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
location string
|
||||
}{
|
||||
{"add valid client", data, contentType, token, http.StatusCreated, fmt.Sprintf("/clients/%s", id)},
|
||||
{"add client with invalid data", invalidData, contentType, token, http.StatusBadRequest, ""},
|
||||
{"add client with invalid auth token", data, contentType, invalidToken, http.StatusForbidden, ""},
|
||||
{"add client with invalid request format", "}", contentType, token, http.StatusBadRequest, ""},
|
||||
{"add client with empty JSON request", "{}", contentType, token, http.StatusBadRequest, ""},
|
||||
{"add client with empty request", "", contentType, token, http.StatusBadRequest, ""},
|
||||
{"add client with missing content type", data, "", token, http.StatusUnsupportedMediaType, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/clients", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
location := res.Header.Get("Location")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.location, location, fmt.Sprintf("%s: expected location %s got %s", tc.desc, tc.location, location))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
data := toJSON(client)
|
||||
invalidData := toJSON(clients.Client{
|
||||
Type: "foo",
|
||||
Name: client.Name,
|
||||
Payload: client.Payload,
|
||||
})
|
||||
id, _ := svc.AddClient(token, client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
id string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"update existing client", data, id, contentType, token, http.StatusOK},
|
||||
{"update non-existent client", data, wrongID, contentType, token, http.StatusNotFound},
|
||||
{"update client with invalid id", data, "1", contentType, token, http.StatusNotFound},
|
||||
{"update client with invalid data", invalidData, id, contentType, token, http.StatusBadRequest},
|
||||
{"update client with invalid user token", data, id, contentType, invalidToken, http.StatusForbidden},
|
||||
{"update client with invalid data format", "{", id, contentType, token, http.StatusBadRequest},
|
||||
{"update client with empty JSON request", "{}", id, contentType, token, http.StatusBadRequest},
|
||||
{"update client with empty request", "", id, contentType, token, http.StatusBadRequest},
|
||||
{"update client with missing content type", data, id, "", token, http.StatusUnsupportedMediaType},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
fmt.Println(req.url)
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
id, _ := svc.AddClient(token, client)
|
||||
|
||||
client.ID = id
|
||||
client.Key = id
|
||||
data := toJSON(client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
res string
|
||||
}{
|
||||
{"view existing client", id, token, http.StatusOK, data},
|
||||
{"view non-existent client", wrongID, token, http.StatusNotFound, ""},
|
||||
{"view client by passing invalid id", "1", token, http.StatusNotFound, ""},
|
||||
{"view client by passing invalid token", id, invalidToken, http.StatusForbidden, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
data := strings.Trim(string(body), "\n")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.res, data, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListClients(t *testing.T) {
|
||||
noClientsToken := "no_clients_token"
|
||||
svc := newService(map[string]string{
|
||||
token: email,
|
||||
noClientsToken: "no_clients_user@example.com",
|
||||
})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
data := []clients.Client{}
|
||||
for i := 0; i < 101; i++ {
|
||||
id, _ := svc.AddClient(token, client)
|
||||
client.ID = id
|
||||
client.Key = id
|
||||
data = append(data, client)
|
||||
}
|
||||
clientURL := fmt.Sprintf("%s/clients", ts.URL)
|
||||
cases := []struct {
|
||||
desc string
|
||||
auth string
|
||||
status int
|
||||
url string
|
||||
res []clients.Client
|
||||
}{
|
||||
{"get a list of clients", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 5), data[0:5]},
|
||||
{"get a list of clients with invalid token", invalidToken, http.StatusForbidden, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 1), nil},
|
||||
{"get a list of clients with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, -1, 5), nil},
|
||||
{"get a list of clients with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 1, -5), nil},
|
||||
{"get a list of clients with zero limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 1, 0), nil},
|
||||
{"get a list of clients with no offset provided", token, http.StatusOK, fmt.Sprintf("%s?limit=%d", clientURL, 5), data[0:5]},
|
||||
{"get a list of clients with no limit provided", token, http.StatusOK, fmt.Sprintf("%s?offset=%d", clientURL, 1), data[1:11]},
|
||||
{"get a list of clients with redundant query params", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d&value=something", clientURL, 0, 5), data[0:5]},
|
||||
{"get a list of clients with limit greater than max", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 110), nil},
|
||||
{"get a list of clients with default URL", token, http.StatusOK, fmt.Sprintf("%s%s", clientURL, ""), data[0:10]},
|
||||
{"get a list of clients with invalid URL", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?%%"), nil},
|
||||
{"get a list of clients with invalid number of params", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=4&limit=4&limit=5&offset=5"), nil},
|
||||
{"get a list of clients with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=e&limit=5"), nil},
|
||||
{"get a list of clients with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=5&limit=e"), nil},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: tc.url,
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
var data map[string][]clients.Client
|
||||
json.NewDecoder(res.Body).Decode(&data)
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.ElementsMatch(t, tc.res, data["clients"], fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data["clients"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
id, _ := svc.AddClient(token, client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"delete existing client", id, token, http.StatusNoContent},
|
||||
{"delete non-existent client", wrongID, token, http.StatusNoContent},
|
||||
{"delete client with invalid id", "1", token, http.StatusNoContent},
|
||||
{"delete client with invalid token", id, invalidToken, http.StatusForbidden},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
data := toJSON(channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
location string
|
||||
}{
|
||||
{"create new channel", data, contentType, token, http.StatusCreated, fmt.Sprintf("/channels/%s", id)},
|
||||
{"create new channel with invalid token", data, contentType, invalidToken, http.StatusForbidden, ""},
|
||||
{"create new channel with invalid data format", "{", contentType, token, http.StatusBadRequest, ""},
|
||||
{"create new channel with empty JSON request", "{}", contentType, token, http.StatusCreated, "/channels/123e4567-e89b-12d3-a456-000000000002"},
|
||||
{"create new channel with empty request", "", contentType, token, http.StatusBadRequest, ""},
|
||||
{"create new channel with missing content type", data, "", token, http.StatusUnsupportedMediaType, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/channels", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
location := res.Header.Get("Location")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.location, location, fmt.Sprintf("%s: expected location %s got %s", tc.desc, tc.location, location))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
updateData := toJSON(map[string]string{
|
||||
"name": "updated_channel",
|
||||
})
|
||||
id, _ := svc.CreateChannel(token, channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
id string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"update existing channel", updateData, id, contentType, token, http.StatusOK},
|
||||
{"update non-existing channel", updateData, wrongID, contentType, token, http.StatusNotFound},
|
||||
{"update channel with invalid token", updateData, id, contentType, invalidToken, http.StatusForbidden},
|
||||
{"update channel with invalid id", updateData, "1", contentType, token, http.StatusNotFound},
|
||||
{"update channel with invalid data format", "}", id, contentType, token, http.StatusBadRequest},
|
||||
{"update channel with empty JSON object", "{}", id, contentType, token, http.StatusOK},
|
||||
{"update channel with empty request", "", id, contentType, token, http.StatusBadRequest},
|
||||
{"update channel with missing content type", updateData, id, "", token, http.StatusUnsupportedMediaType},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
id, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = id
|
||||
data := toJSON(channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
res string
|
||||
}{
|
||||
{"view existing channel", id, token, http.StatusOK, data},
|
||||
{"view non-existent channel", wrongID, token, http.StatusNotFound, ""},
|
||||
{"view channel with invalid id", "1", token, http.StatusNotFound, ""},
|
||||
{"view channel with invalid token", id, invalidToken, http.StatusForbidden, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
body := strings.Trim(string(data), "\n")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.res, body, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListChannels(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
channels := []clients.Channel{}
|
||||
for i := 0; i < 101; i++ {
|
||||
id, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = id
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
channelURL := fmt.Sprintf("%s/channels", ts.URL)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
auth string
|
||||
status int
|
||||
url string
|
||||
res []clients.Channel
|
||||
}{
|
||||
{"get a list of channels", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 6), channels[0:6]},
|
||||
{"get a list of channels with invalid token", invalidToken, http.StatusForbidden, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 1), nil},
|
||||
{"get a list of channels with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, -1, 5), nil},
|
||||
{"get a list of channels with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, -1, 5), nil},
|
||||
{"get a list of channels with zero limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 1, 0), nil},
|
||||
{"get a list of channels with no offset provided", token, http.StatusOK, fmt.Sprintf("%s?limit=%d", channelURL, 5), channels[0:5]},
|
||||
{"get a list of channels with no limit provided", token, http.StatusOK, fmt.Sprintf("%s?offset=%d", channelURL, 1), channels[1:11]},
|
||||
{"get a list of channels with redundant query params", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d&value=something", channelURL, 0, 5), channels[0:5]},
|
||||
{"get a list of channels with limit greater than max", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 110), nil},
|
||||
{"get a list of channels with default URL", token, http.StatusOK, fmt.Sprintf("%s%s", channelURL, ""), channels[0:10]},
|
||||
{"get a list of channels with invalid URL", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?%%"), nil},
|
||||
{"get a list of channels with invalid number of params", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=4&limit=4&limit=5&offset=5"), nil},
|
||||
{"get a list of channels with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=e&limit=5"), nil},
|
||||
{"get a list of channels with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=5&limit=e"), nil},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: tc.url,
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
var body map[string][]clients.Channel
|
||||
json.NewDecoder(res.Body).Decode(&body)
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.ElementsMatch(t, tc.res, body["channels"], fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, body["channels"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
id, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = id
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"remove existing channel", channel.ID, token, http.StatusNoContent},
|
||||
{"remove non-existent channel", channel.ID, token, http.StatusNoContent},
|
||||
{"remove channel with invalid id", wrongID, token, http.StatusNoContent},
|
||||
{"remove channel with invalid token", channel.ID, "invalidToken", http.StatusForbidden},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
otherToken := "other_token"
|
||||
otherEmail := "other_user@example.com"
|
||||
svc := newService(map[string]string{
|
||||
token: email,
|
||||
otherToken: otherEmail,
|
||||
})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
|
||||
otherClientID, _ := svc.AddClient(otherToken, client)
|
||||
otherChanID, _ := svc.CreateChannel(otherToken, channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chanID string
|
||||
clientID string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"connect existing client to existing channel", chanID, clientID, token, http.StatusOK},
|
||||
{"connect existing client to non-existent channel", wrongID, clientID, token, http.StatusNotFound},
|
||||
{"connect client with invalid id to channel", chanID, "1", token, http.StatusNotFound},
|
||||
{"connect client to channel with invalid id", "1", clientID, token, http.StatusNotFound},
|
||||
{"connect existing client to existing channel with invalid token", chanID, clientID, invalidToken, http.StatusForbidden},
|
||||
{"connect client from owner to channel of other user", otherChanID, clientID, token, http.StatusNotFound},
|
||||
{"connect client from other user to owner's channel", chanID, otherClientID, token, http.StatusNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/channels/%s/clients/%s", ts.URL, tc.chanID, tc.clientID),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnnect(t *testing.T) {
|
||||
otherToken := "other_token"
|
||||
otherEmail := "other_user@example.com"
|
||||
svc := newService(map[string]string{
|
||||
token: email,
|
||||
otherToken: otherEmail,
|
||||
})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
svc.Connect(token, chanID, clientID)
|
||||
otherClientID, _ := svc.AddClient(otherToken, client)
|
||||
otherChanID, _ := svc.CreateChannel(otherToken, channel)
|
||||
svc.Connect(otherToken, otherChanID, otherClientID)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chanID string
|
||||
clientID string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"disconnect connected client from channel", chanID, clientID, token, http.StatusNoContent},
|
||||
{"disconnect non-connected client from channel", chanID, clientID, token, http.StatusNotFound},
|
||||
{"disconnect non-existent client from channel", chanID, "1", token, http.StatusNotFound},
|
||||
{"disconnect client from non-existent channel", "1", clientID, token, http.StatusNotFound},
|
||||
{"disconnect client from channel with invalid token", chanID, clientID, invalidToken, http.StatusForbidden},
|
||||
{"disconnect owner's client from someone elses channel", otherChanID, clientID, token, http.StatusNotFound},
|
||||
{"disconnect other's client from owner's channel", chanID, otherClientID, token, http.StatusNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/channels/%s/clients/%s", ts.URL, tc.chanID, tc.clientID),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
const maxLimitSize = 100
|
||||
|
||||
type apiReq interface {
|
||||
validate() error
|
||||
}
|
||||
|
||||
type identityReq struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (req identityReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type addClientReq struct {
|
||||
key string
|
||||
client clients.Client
|
||||
}
|
||||
|
||||
func (req addClientReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return req.client.Validate()
|
||||
}
|
||||
|
||||
type updateClientReq struct {
|
||||
key string
|
||||
id string
|
||||
client clients.Client
|
||||
}
|
||||
|
||||
func (req updateClientReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return req.client.Validate()
|
||||
}
|
||||
|
||||
type createChannelReq struct {
|
||||
key string
|
||||
channel clients.Channel
|
||||
}
|
||||
|
||||
func (req createChannelReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateChannelReq struct {
|
||||
key string
|
||||
id string
|
||||
channel clients.Channel
|
||||
}
|
||||
|
||||
func (req updateChannelReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type viewResourceReq struct {
|
||||
key string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req viewResourceReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listResourcesReq struct {
|
||||
key string
|
||||
offset int
|
||||
limit int
|
||||
}
|
||||
|
||||
func (req *listResourcesReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.offset >= 0 && req.limit > 0 && req.limit <= maxLimitSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
return clients.ErrMalformedEntity
|
||||
}
|
||||
|
||||
type connectionReq struct {
|
||||
key string
|
||||
chanID string
|
||||
clientID string
|
||||
}
|
||||
|
||||
func (req connectionReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.chanID) || !govalidator.IsUUID(req.clientID) {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const wrong string = "?"
|
||||
|
||||
var (
|
||||
client = clients.Client{Type: "app"}
|
||||
channel = clients.Channel{}
|
||||
)
|
||||
|
||||
func TestIdentityReqValidation(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"non-empty token": {uuid.NewV4().String(), nil},
|
||||
"empty token": {"", clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := identityReq{tc.key}
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddClientReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
client clients.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid client addition request": {client, key, nil},
|
||||
"missing token": {client, "", clients.ErrUnauthorizedAccess},
|
||||
"wrong client type": {clients.Client{Type: wrong}, key, clients.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := addClientReq{
|
||||
key: tc.key,
|
||||
client: tc.client,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClientReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
client clients.Client
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid client update request": {client, id, key, nil},
|
||||
"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 {
|
||||
req := updateClientReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
client: tc.client,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannelReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
channel clients.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid channel creation request": {channel, key, nil},
|
||||
"missing token": {channel, "", clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := createChannelReq{
|
||||
key: tc.key,
|
||||
channel: tc.channel,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannelReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
channel clients.Channel
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid channel update request": {channel, id, key, nil},
|
||||
"non-uuid channel ID": {channel, wrong, key, clients.ErrNotFound},
|
||||
"missing token": {channel, id, "", clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := updateChannelReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
channel: tc.channel,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewResourceReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid resource viewing request": {id, key, nil},
|
||||
"missing token": {id, "", clients.ErrUnauthorizedAccess},
|
||||
"non-uuid resource ID": {wrong, key, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := viewResourceReq{tc.key, tc.id}
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListResourcesReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
value := 10
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
offset int
|
||||
limit int
|
||||
err error
|
||||
}{
|
||||
"valid listing request": {key, value, value, nil},
|
||||
"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 {
|
||||
req := listResourcesReq{
|
||||
key: tc.key,
|
||||
offset: tc.offset,
|
||||
limit: tc.limit,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var _ clients.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc clients.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and
|
||||
// latency.
|
||||
func MetricsMiddleware(svc clients.Service, counter metrics.Counter, latency metrics.Histogram) clients.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.AddClient(key, client)
|
||||
}
|
||||
|
||||
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())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.UpdateClient(key, client)
|
||||
}
|
||||
|
||||
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())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ViewClient(key, id)
|
||||
}
|
||||
|
||||
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())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListClients(key, offset, limit)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RemoveClient(key string, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "remove_client").Add(1)
|
||||
ms.latency.With("method", "remove_client").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RemoveClient(key, id)
|
||||
}
|
||||
|
||||
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())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.CreateChannel(key, channel)
|
||||
}
|
||||
|
||||
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())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.UpdateChannel(key, channel)
|
||||
}
|
||||
|
||||
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())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ViewChannel(key, id)
|
||||
}
|
||||
|
||||
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())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListChannels(key, offset, limit)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RemoveChannel(key string, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "remove_channel").Add(1)
|
||||
ms.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RemoveChannel(key, id)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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) CanAccess(key string, id string) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "can_access").Add(1)
|
||||
ms.latency.With("method", "can_access").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.CanAccess(key, id)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Package bcrypt provides a hasher implementation utilising bcrypt.
|
||||
package bcrypt
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const cost int = 10
|
||||
|
||||
var _ clients.Hasher = (*bcryptHasher)(nil)
|
||||
|
||||
type bcryptHasher struct{}
|
||||
|
||||
// New instantiates a bcrypt-based hasher implementation.
|
||||
func New() clients.Hasher {
|
||||
return &bcryptHasher{}
|
||||
}
|
||||
|
||||
func (bh *bcryptHasher) Hash(pwd string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), cost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
func (bh *bcryptHasher) Compare(plain, hashed string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain))
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package clients
|
||||
|
||||
// Channel represents a Mainflux "communication group". This group contains the
|
||||
// clients that can exchange messages between eachother.
|
||||
type Channel struct {
|
||||
ID string `json:"id"`
|
||||
Owner string `json:"-"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Clients []Client `json:"connected,omitempty"`
|
||||
}
|
||||
|
||||
// ChannelRepository specifies a channel persistence API.
|
||||
type ChannelRepository interface {
|
||||
// Save persists the channel. Successful operation is indicated by unique
|
||||
// identifier accompanied by nil error response. A non-nil error is
|
||||
// returned to indicate operation failure.
|
||||
Save(Channel) (string, error)
|
||||
|
||||
// Update performs an update to the existing channel. A non-nil error is
|
||||
// returned to indicate operation failure.
|
||||
Update(Channel) error
|
||||
|
||||
// One retrieves the channel having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
One(string, string) (Channel, error)
|
||||
|
||||
// All retrieves the subset of channels owned by the specified user.
|
||||
All(string, int, int) []Channel
|
||||
|
||||
// Remove removes the channel having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
Remove(string, string) error
|
||||
|
||||
// Connect adds client to the channel's list of connected clients.
|
||||
Connect(string, string, string) error
|
||||
|
||||
// Disconnect removes client from the channel's list of connected
|
||||
// clients.
|
||||
Disconnect(string, string, string) error
|
||||
|
||||
// HasClient determines whether the client with the provided identifier, is
|
||||
// "connected" to the specified channel.
|
||||
HasClient(string, string) bool
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package clients
|
||||
|
||||
import "strings"
|
||||
|
||||
// Client represents a Mainflux client. Each client is owned by one user, and
|
||||
// it is assigned with the unique identifier and (temporary) access key.
|
||||
type Client struct {
|
||||
ID string `json:"id"`
|
||||
Owner string `json:"-"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Payload string `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
var clientTypes = map[string]bool{
|
||||
"app": true,
|
||||
"device": true,
|
||||
}
|
||||
|
||||
// Validate returns an error if client representation is invalid.
|
||||
func (c *Client) Validate() error {
|
||||
if c.Type = strings.ToLower(c.Type); !clientTypes[c.Type] {
|
||||
return ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClientRepository specifies a client persistence API.
|
||||
type ClientRepository interface {
|
||||
// ID generates new resource identifier.
|
||||
ID() string
|
||||
|
||||
// Save persists the client. Successful operation is indicated by non-nil
|
||||
// error response.
|
||||
Save(Client) error
|
||||
|
||||
// Update performs an update to the existing client. A non-nil error is
|
||||
// returned to indicate operation failure.
|
||||
Update(Client) error
|
||||
|
||||
// One retrieves the client having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
One(string, string) (Client, error)
|
||||
|
||||
// All retrieves the subset of clients owned by the specified user.
|
||||
All(string, int, int) []Client
|
||||
|
||||
// Remove removes the client having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
Remove(string, string) error
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package clients contains the domain concept definitions needed to support
|
||||
// Mainflux clients service functionality.
|
||||
package clients
|
||||
@@ -1,74 +0,0 @@
|
||||
// Package jwt provides a JWT identity provider.
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
const (
|
||||
issuer string = "mainflux"
|
||||
duration time.Duration = 10 * time.Hour
|
||||
)
|
||||
|
||||
var _ clients.IdentityProvider = (*jwtIdentityProvider)(nil)
|
||||
|
||||
type jwtIdentityProvider struct {
|
||||
secret string
|
||||
}
|
||||
|
||||
// New instantiates a JWT identity provider.
|
||||
func New(secret string) clients.IdentityProvider {
|
||||
return &jwtIdentityProvider{secret}
|
||||
}
|
||||
|
||||
func (idp *jwtIdentityProvider) TemporaryKey(id string) (string, error) {
|
||||
now := time.Now().UTC()
|
||||
exp := now.Add(duration)
|
||||
|
||||
claims := jwt.StandardClaims{
|
||||
Subject: id,
|
||||
Issuer: issuer,
|
||||
IssuedAt: now.Unix(),
|
||||
ExpiresAt: exp.Unix(),
|
||||
}
|
||||
|
||||
return idp.jwt(claims)
|
||||
}
|
||||
|
||||
func (idp *jwtIdentityProvider) PermanentKey(id string) (string, error) {
|
||||
claims := jwt.StandardClaims{
|
||||
Subject: id,
|
||||
Issuer: issuer,
|
||||
IssuedAt: time.Now().UTC().Unix(),
|
||||
}
|
||||
|
||||
return idp.jwt(claims)
|
||||
}
|
||||
|
||||
func (idp *jwtIdentityProvider) jwt(claims jwt.StandardClaims) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(idp.secret))
|
||||
}
|
||||
|
||||
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, clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return []byte(idp.secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return claims["sub"].(string), nil
|
||||
}
|
||||
|
||||
return "", clients.ErrUnauthorizedAccess
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var _ clients.ChannelRepository = (*channelRepositoryMock)(nil)
|
||||
|
||||
const chanID = "123e4567-e89b-12d3-a456-"
|
||||
|
||||
type channelRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
channels map[string]clients.Channel
|
||||
clients clients.ClientRepository
|
||||
}
|
||||
|
||||
// NewChannelRepository creates in-memory channel repository.
|
||||
func NewChannelRepository(repo clients.ClientRepository) clients.ChannelRepository {
|
||||
return &channelRepositoryMock{
|
||||
channels: make(map[string]clients.Channel),
|
||||
clients: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Save(channel clients.Channel) (string, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
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 clients.Channel) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
dbKey := key(channel.Owner, channel.ID)
|
||||
|
||||
if _, ok := crm.channels[dbKey]; !ok {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
crm.channels[dbKey] = channel
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) One(owner, id string) (clients.Channel, error) {
|
||||
if c, ok := crm.channels[key(owner, id)]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return clients.Channel{}, clients.ErrNotFound
|
||||
}
|
||||
|
||||
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([]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)
|
||||
|
||||
for k, v := range crm.channels {
|
||||
if strings.HasPrefix(k, prefix) && v.ID >= first && v.ID < last {
|
||||
channels = append(channels, v)
|
||||
}
|
||||
}
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Remove(owner, id string) error {
|
||||
delete(crm.channels, key(owner, id))
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
channel.Clients = append(channel.Clients, client)
|
||||
return crm.Update(channel)
|
||||
}
|
||||
|
||||
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 clients.ErrNotFound
|
||||
}
|
||||
|
||||
connected := make([]clients.Client, len(channel.Clients)-1)
|
||||
for _, client := range channel.Clients {
|
||||
if client.ID != clientID {
|
||||
connected = append(connected, client)
|
||||
}
|
||||
}
|
||||
|
||||
channel.Clients = connected
|
||||
return crm.Update(channel)
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) HasClient(channel, client string) bool {
|
||||
// This obscure way to examine map keys is enforced by the key structure
|
||||
// itself (see mocks/commons.go).
|
||||
suffix := fmt.Sprintf("-%s", channel)
|
||||
|
||||
for k, v := range crm.channels {
|
||||
if strings.HasSuffix(k, suffix) {
|
||||
for _, c := range v.Clients {
|
||||
if c.ID == client {
|
||||
return true
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var _ clients.ClientRepository = (*clientRepositoryMock)(nil)
|
||||
|
||||
const cliID = "123e4567-e89b-12d3-a456-"
|
||||
|
||||
type clientRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
clients map[string]clients.Client
|
||||
}
|
||||
|
||||
// NewClientRepository creates in-memory client repository.
|
||||
func NewClientRepository() clients.ClientRepository {
|
||||
return &clientRepositoryMock{
|
||||
clients: make(map[string]clients.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) ID() string {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.counter++
|
||||
return fmt.Sprintf("%s%012d", cliID, crm.counter)
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Save(client clients.Client) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.clients[key(client.Owner, client.ID)] = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 clients.ErrNotFound
|
||||
}
|
||||
|
||||
crm.clients[dbKey] = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) One(owner, id string) (clients.Client, error) {
|
||||
if c, ok := crm.clients[key(owner, id)]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return clients.Client{}, clients.ErrNotFound
|
||||
}
|
||||
|
||||
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([]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)
|
||||
|
||||
for k, v := range crm.clients {
|
||||
if strings.HasPrefix(k, prefix) && v.ID >= first && v.ID < last {
|
||||
clients = append(clients, v)
|
||||
}
|
||||
}
|
||||
|
||||
return clients
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Remove(owner, id string) error {
|
||||
delete(crm.clients, key(owner, id))
|
||||
return nil
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var (
|
||||
_ clients.Hasher = (*hasherMock)(nil)
|
||||
_ clients.IdentityProvider = (*identityProviderMock)(nil)
|
||||
)
|
||||
|
||||
type hasherMock struct{}
|
||||
|
||||
func (hm *hasherMock) Hash(pwd string) (string, error) {
|
||||
return pwd, nil
|
||||
}
|
||||
|
||||
func (hm *hasherMock) Compare(plain, hashed string) error {
|
||||
if plain != hashed {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type identityProviderMock struct{}
|
||||
|
||||
func (idp *identityProviderMock) TemporaryKey(id string) (string, error) {
|
||||
if id == "" {
|
||||
return "", clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (idp *identityProviderMock) PermanentKey(id string) (string, error) {
|
||||
return idp.TemporaryKey(id)
|
||||
}
|
||||
|
||||
func (idp *identityProviderMock) Identity(key string) (string, error) {
|
||||
return idp.TemporaryKey(key)
|
||||
}
|
||||
|
||||
// NewHasher creates "no-op" hasher for test purposes. This implementation will
|
||||
// return secrets without changing them.
|
||||
func NewHasher() clients.Hasher {
|
||||
return &hasherMock{}
|
||||
}
|
||||
|
||||
// NewIdentityProvider creates "mirror" identity provider, i.e. generated
|
||||
// token will hold value provided by the caller.
|
||||
func NewIdentityProvider() clients.IdentityProvider {
|
||||
return &identityProviderMock{}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
var _ clients.ChannelRepository = (*channelRepository)(nil)
|
||||
|
||||
const (
|
||||
errDuplicate = "unique_violation"
|
||||
errFK = "foreign_key_violation"
|
||||
)
|
||||
|
||||
type channelRepository struct {
|
||||
db *sql.DB
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
// NewChannelRepository instantiates a PostgreSQL implementation of channel
|
||||
// repository.
|
||||
func NewChannelRepository(db *sql.DB, log logger.Logger) clients.ChannelRepository {
|
||||
return &channelRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr channelRepository) Save(channel clients.Channel) (string, error) {
|
||||
channel.ID = uuid.NewV4().String()
|
||||
|
||||
q := `INSERT INTO channels (id, owner, name) VALUES ($1, $2, $3)`
|
||||
|
||||
_, err := cr.db.Exec(q, channel.ID, channel.Owner, channel.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return channel.ID, nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) Update(channel clients.Channel) error {
|
||||
q := `UPDATE channels SET name = $1 WHERE owner = $2 AND id = $3;`
|
||||
|
||||
res, err := cr.db.Exec(q, channel.Name, channel.Owner, channel.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) One(owner, id string) (clients.Channel, error) {
|
||||
q := `SELECT name FROM channels WHERE id = $1 AND owner = $2`
|
||||
channel := clients.Channel{ID: id, Owner: owner}
|
||||
if err := cr.db.QueryRow(q, id, owner).Scan(&channel.Name); err != nil {
|
||||
empty := clients.Channel{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, clients.ErrNotFound
|
||||
}
|
||||
return empty, err
|
||||
}
|
||||
|
||||
qr := `SELECT id, type, name, key, payload FROM clients cli
|
||||
INNER JOIN connections conn
|
||||
ON cli.id = conn.client_id AND cli.owner = conn.client_owner
|
||||
WHERE conn.channel_id = $1 AND conn.channel_owner = $2`
|
||||
|
||||
rows, err := cr.db.Query(qr, id, owner)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return clients.Channel{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
c := clients.Client{Owner: owner}
|
||||
if err = rows.Scan(&c.ID, &c.Name, &c.Type, &c.Key, &c.Payload); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected client due to %s", err))
|
||||
return clients.Channel{}, err
|
||||
}
|
||||
channel.Clients = append(channel.Clients, c)
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) All(owner string, offset, limit int) []clients.Channel {
|
||||
q := `SELECT id, name FROM channels WHERE owner = $1 LIMIT $2 OFFSET $3`
|
||||
items := []clients.Channel{}
|
||||
|
||||
rows, err := cr.db.Query(q, owner, limit, offset)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve channels due to %s", err))
|
||||
return []clients.Channel{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
c := clients.Channel{Owner: owner}
|
||||
if err = rows.Scan(&c.ID, &c.Name); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved channel due to %s", err))
|
||||
return []clients.Channel{}
|
||||
}
|
||||
items = append(items, c)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (cr channelRepository) Remove(owner, id string) error {
|
||||
q := `DELETE FROM channels WHERE id = $1 AND owner = $2`
|
||||
cr.db.Exec(q, id, owner)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) Connect(owner, chanID, clientID string) error {
|
||||
q := `INSERT INTO connections (channel_id, channel_owner, client_id, client_owner) VALUES ($1, $2, $3, $2)`
|
||||
|
||||
if _, err := cr.db.Exec(q, chanID, owner, clientID); err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
|
||||
if ok && errFK == pqErr.Code.Name() {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
// connect is idempotent
|
||||
if ok && errDuplicate == pqErr.Code.Name() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) Disconnect(owner, chanID, clientID string) error {
|
||||
q := `DELETE FROM connections
|
||||
WHERE channel_id = $1 AND channel_owner = $2
|
||||
AND client_id = $3 AND client_owner = $2`
|
||||
|
||||
res, err := cr.db.Exec(q, chanID, owner, clientID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) HasClient(chanID, clientID string) bool {
|
||||
q := "SELECT EXISTS (SELECT 1 FROM connections WHERE channel_id = $1 AND client_id = $2);"
|
||||
|
||||
exists := false
|
||||
if err := cr.db.QueryRow(q, chanID, clientID).Scan(&exists); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to check client existence due to %s", err))
|
||||
}
|
||||
return exists
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
channel := clients.Channel{Owner: email}
|
||||
|
||||
channelRepo := postgres.NewChannelRepository(db, testLog)
|
||||
|
||||
_, 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"
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db, testLog)
|
||||
|
||||
c := clients.Channel{Owner: email}
|
||||
id, _ := chanRepo.Save(c)
|
||||
c.ID = id
|
||||
|
||||
cases := map[string]struct {
|
||||
channel clients.Channel
|
||||
err error
|
||||
}{
|
||||
"existing channel": {c, nil},
|
||||
"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 {
|
||||
err := chanRepo.Update(tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleChannelRetrieval(t *testing.T) {
|
||||
email := "channel-single-retrieval@example.com"
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db, testLog)
|
||||
|
||||
c := clients.Channel{Owner: email}
|
||||
id, _ := chanRepo.Save(c)
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
ID string
|
||||
err error
|
||||
}{
|
||||
"existing user": {c.Owner, id, nil},
|
||||
"existing user, non-existing channel": {c.Owner, wrong, clients.ErrNotFound},
|
||||
"non-existing owner": {wrong, id, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := chanRepo.One(tc.owner, tc.ID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiChannelRetrieval(t *testing.T) {
|
||||
email := "channel-multi-retrieval@example.com"
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db, testLog)
|
||||
|
||||
n := 10
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
c := clients.Channel{Owner: email}
|
||||
chanRepo.Save(c)
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
offset int
|
||||
limit int
|
||||
size int
|
||||
}{
|
||||
"existing owner": {email, 0, n, n},
|
||||
"non-existing owner": {wrong, 1, 6, 0},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
size := len(chanRepo.All(tc.owner, tc.offset, tc.limit))
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelRemoval(t *testing.T) {
|
||||
email := "channel-removal@example.com"
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db, testLog)
|
||||
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 {
|
||||
t.Fatalf("#%d: failed to remove channel due to: %s", i, err)
|
||||
}
|
||||
|
||||
if _, err := chanRepo.One(email, chanID); err != clients.ErrNotFound {
|
||||
t.Fatalf("#%d: expected %s got %s", i, clients.ErrNotFound, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelConnect(t *testing.T) {
|
||||
email := "channel-connect@example.com"
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db, testLog)
|
||||
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
owner string
|
||||
chanID string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
{"existing user, channel and client", email, chanID, client.ID, nil},
|
||||
{"connected 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 _, tc := range cases {
|
||||
err := chanRepo.Connect(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))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDisconnect(t *testing.T) {
|
||||
email := "channel-disconnect@example.com"
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db, testLog)
|
||||
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
|
||||
|
||||
chanRepo.Connect(email, chanID, client.ID)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
owner string
|
||||
chanID string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
{"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)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelAccessCheck(t *testing.T) {
|
||||
email := "channel-access-check@example.com"
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db, testLog)
|
||||
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
|
||||
|
||||
chanRepo.Connect(email, chanID, client.ID)
|
||||
|
||||
cases := map[string]struct {
|
||||
chanID string
|
||||
clientID string
|
||||
hasAccess bool
|
||||
}{
|
||||
"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)
|
||||
assert.Equal(t, tc.hasAccess, hasAccess, fmt.Sprintf("%s: expected %t got %t\n", desc, tc.hasAccess, hasAccess))
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/lib/pq" // required for DB access
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
var _ clients.ClientRepository = (*clientRepository)(nil)
|
||||
|
||||
type clientRepository struct {
|
||||
db *sql.DB
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
// NewClientRepository instantiates a PostgreSQL implementation of client
|
||||
// repository.
|
||||
func NewClientRepository(db *sql.DB, log logger.Logger) clients.ClientRepository {
|
||||
return &clientRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr clientRepository) ID() string {
|
||||
return uuid.NewV4().String()
|
||||
}
|
||||
|
||||
func (cr clientRepository) Save(client clients.Client) error {
|
||||
q := `INSERT INTO clients (id, owner, type, name, key, payload) VALUES ($1, $2, $3, $4, $5, $6)`
|
||||
_, err := cr.db.Exec(q, client.ID, client.Owner, client.Type, client.Name, client.Key, client.Payload)
|
||||
return err
|
||||
}
|
||||
|
||||
func (cr clientRepository) Update(client clients.Client) error {
|
||||
q := `UPDATE clients SET name = $1, payload = $2 WHERE owner = $3 AND id = $4;`
|
||||
|
||||
res, err := cr.db.Exec(q, client.Name, client.Payload, client.Owner, client.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr clientRepository) One(owner, id string) (clients.Client, error) {
|
||||
q := `SELECT name, type, key, payload FROM clients WHERE id = $1 AND owner = $2`
|
||||
client := clients.Client{ID: id, Owner: owner}
|
||||
err := cr.db.
|
||||
QueryRow(q, id, owner).
|
||||
Scan(&client.Name, &client.Type, &client.Key, &client.Payload)
|
||||
|
||||
if err != nil {
|
||||
empty := clients.Client{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, clients.ErrNotFound
|
||||
}
|
||||
return empty, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (cr clientRepository) All(owner string, offset, limit int) []clients.Client {
|
||||
q := `SELECT id, name, type, key, payload FROM clients WHERE owner = $1 LIMIT $2 OFFSET $3`
|
||||
items := []clients.Client{}
|
||||
|
||||
rows, err := cr.db.Query(q, owner, limit, offset)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve clients due to %s", err))
|
||||
return []clients.Client{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
c := clients.Client{Owner: owner}
|
||||
if err = rows.Scan(&c.ID, &c.Name, &c.Type, &c.Key, &c.Payload); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved client due to %s", err))
|
||||
return []clients.Client{}
|
||||
}
|
||||
items = append(items, c)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (cr clientRepository) Remove(owner, id string) error {
|
||||
q := `DELETE FROM clients WHERE id = $1 AND owner = $2`
|
||||
cr.db.Exec(q, id, owner)
|
||||
return nil
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
|
||||
c := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
clientRepo.Save(c)
|
||||
|
||||
cases := map[string]struct {
|
||||
client clients.Client
|
||||
err error
|
||||
}{
|
||||
"existing client": {c, nil},
|
||||
"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 {
|
||||
err := clientRepo.Update(tc.client)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleClientRetrieval(t *testing.T) {
|
||||
email := "client-single-retrieval@example.com"
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
|
||||
c := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
clientRepo.Save(c)
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
ID string
|
||||
err error
|
||||
}{
|
||||
"existing user": {c.Owner, c.ID, nil},
|
||||
"existing user, non-existing client": {c.Owner, wrong, clients.ErrNotFound},
|
||||
"non-existing owner": {wrong, c.ID, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := clientRepo.One(tc.owner, tc.ID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiClientRetrieval(t *testing.T) {
|
||||
email := "client-multi-retrieval@example.com"
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
|
||||
n := 10
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
c := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
clientRepo.Save(c)
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
offset int
|
||||
limit int
|
||||
size int
|
||||
}{
|
||||
"existing owner, retrieve all": {email, 0, n, n},
|
||||
"existing owner, retrieve subset": {email, 1, 6, 6},
|
||||
"non-existing owner": {wrong, 1, 6, 0},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
n := len(clientRepo.All(tc.owner, tc.offset, tc.limit))
|
||||
assert.Equal(t, tc.size, n, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, n))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRemoval(t *testing.T) {
|
||||
email := "client-removal@example.com"
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
// show that the removal works the same for both existing and non-existing
|
||||
// (removed) client
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := clientRepo.Remove(email, client.ID); err != nil {
|
||||
t.Fatalf("#%d: failed to remove client due to: %s", i, err)
|
||||
}
|
||||
|
||||
if _, err := clientRepo.One(email, client.ID); err != clients.ErrNotFound {
|
||||
t.Fatalf("#%d: expected %s got %s", i, clients.ErrNotFound, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package clients
|
||||
|
||||
// Hasher specifies an API for generating hashes of an arbitrary textual
|
||||
// content.
|
||||
type Hasher interface {
|
||||
// Hash generates the hashed string from plain-text.
|
||||
Hash(string) (string, error)
|
||||
|
||||
// Compare compares plain-text version to the hashed one. An error should
|
||||
// indicate failed comparison.
|
||||
Compare(string, string) error
|
||||
}
|
||||
|
||||
// IdentityProvider specifies an API for identity management via security
|
||||
// tokens.
|
||||
type IdentityProvider interface {
|
||||
// TemporaryKey generates the temporary access token.
|
||||
TemporaryKey(string) (string, error)
|
||||
|
||||
// PermanentKey generates the non-expiring access token.
|
||||
PermanentKey(string) (string, error)
|
||||
|
||||
// Identity extracts the entity identifier given its secret key.
|
||||
Identity(string) (string, error)
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrConflict indicates usage of the existing email during account
|
||||
// registration.
|
||||
ErrConflict = errors.New("email already taken")
|
||||
|
||||
// ErrMalformedEntity indicates malformed entity specification (e.g.
|
||||
// invalid username or password).
|
||||
ErrMalformedEntity = errors.New("malformed entity specification")
|
||||
|
||||
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
|
||||
// when accessing a protected resource.
|
||||
ErrUnauthorizedAccess = errors.New("missing or invalid credentials provided")
|
||||
|
||||
// ErrNotFound indicates a non-existent entity request.
|
||||
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 {
|
||||
// AddClient adds new client to the user identified by the provided key.
|
||||
AddClient(string, Client) (string, error)
|
||||
|
||||
// UpdateClient updates the client identified by the provided ID, that
|
||||
// belongs to the user identified by the provided key.
|
||||
UpdateClient(string, Client) error
|
||||
|
||||
// ViewClient retrieves data about the client identified with the provided
|
||||
// ID, that belongs to the user identified by the provided key.
|
||||
ViewClient(string, string) (Client, error)
|
||||
|
||||
// ListClients retrieves data about subset of clients that belongs to the
|
||||
// user identified by the provided key.
|
||||
ListClients(string, int, int) ([]Client, error)
|
||||
|
||||
// RemoveClient removes the client identified with the provided ID, that
|
||||
// belongs to the user identified by the provided key.
|
||||
RemoveClient(string, string) error
|
||||
|
||||
// CreateChannel adds new channel to the user identified by the provided key.
|
||||
CreateChannel(string, Channel) (string, error)
|
||||
|
||||
// UpdateChannel updates the channel identified by the provided ID, that
|
||||
// belongs to the user identified by the provided key.
|
||||
UpdateChannel(string, Channel) error
|
||||
|
||||
// ViewChannel retrieves data about the channel identified by the provided
|
||||
// ID, that belongs to the user identified by the provided key.
|
||||
ViewChannel(string, string) (Channel, error)
|
||||
|
||||
// ListChannels retrieves data about subset of channels that belongs to the
|
||||
// user identified by the provided key.
|
||||
ListChannels(string, int, int) ([]Channel, error)
|
||||
|
||||
// RemoveChannel removes the client identified by the provided ID, that
|
||||
// belongs to the user identified by the provided key.
|
||||
RemoveChannel(string, string) error
|
||||
|
||||
// Connect adds client to the channel's list of connected clients.
|
||||
Connect(string, string, string) error
|
||||
|
||||
// Disconnect removes client from the channel's list of connected
|
||||
// clients.
|
||||
Disconnect(string, string, string) error
|
||||
|
||||
// CanAccess determines whether the channel can be accessed using the
|
||||
// provided key and returns client's id.
|
||||
CanAccess(string, string) (string, error)
|
||||
}
|
||||
|
||||
var _ Service = (*clientsService)(nil)
|
||||
|
||||
type clientsService struct {
|
||||
users mainflux.UsersServiceClient
|
||||
clients ClientRepository
|
||||
channels ChannelRepository
|
||||
hasher Hasher
|
||||
idp IdentityProvider
|
||||
}
|
||||
|
||||
// New instantiates the clients 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
|
||||
}
|
||||
@@ -1,342 +0,0 @@
|
||||
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": {"", 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))
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
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"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const (
|
||||
defPort int = 5683
|
||||
defNatsURL string = broker.DefaultURL
|
||||
defClientsURL string = "localhost:8181"
|
||||
envPort string = "MF_COAP_ADAPTER_PORT"
|
||||
envNatsURL string = "MF_NATS_URL"
|
||||
envClientsURL string = "MF_CLIENTS_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ClientsURL string
|
||||
NatsURL string
|
||||
Port int
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: defPort,
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
nc, err := broker.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
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)
|
||||
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "coap_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "coap_adapter",
|
||||
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(":%d", cfg.Port)
|
||||
logger.Info(fmt.Sprintf("CoAP adapter service started, exposed port %d", cfg.Port))
|
||||
errs <- api.ListenAndServe(svc, cc, p)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("CoAP adapter terminated: %s", err))
|
||||
}
|
||||
+15
-15
@@ -9,36 +9,36 @@ 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"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/grpc"
|
||||
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
|
||||
defClientsURL string = "localhost:8181"
|
||||
envPort string = "MF_HTTP_ADAPTER_PORT"
|
||||
envNatsURL string = "MF_NATS_URL"
|
||||
envClientsURL string = "MF_CLIENTS_URL"
|
||||
defPort string = "8180"
|
||||
defNatsURL string = broker.DefaultURL
|
||||
defThingsURL string = "localhost:8181"
|
||||
envPort string = "MF_HTTP_ADAPTER_PORT"
|
||||
envNatsURL string = "MF_NATS_URL"
|
||||
envThingsURL string = "MF_THINGS_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ClientsURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
ThingsURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
ThingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
@@ -50,14 +50,14 @@ func main() {
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
|
||||
conn, err := grpc.Dial(cfg.ThingsURL, 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)
|
||||
cc := thingsapi.NewClient(conn)
|
||||
pub := nats.NewMessagePublisher(nc)
|
||||
|
||||
svc := adapter.New(pub)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
influxdata "github.com/influxdata/influxdb/client/v2"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/writers"
|
||||
"github.com/mainflux/mainflux/writers/influxdb"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
name = "influxdb-writer"
|
||||
senML = "out.senml"
|
||||
prefix = "http://"
|
||||
defNatsURL = nats.DefaultURL
|
||||
defPort = "8180"
|
||||
defPointName = "messages"
|
||||
defDBName = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "8086"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envPort = "MF_INFLUX_WRITER_PORT"
|
||||
envDBName = "MF_INFLUX_WRITER_DB_NAME"
|
||||
envDBHost = "MF_INFLUX_WRITER_DB_HOST"
|
||||
envDBPort = "MF_INFLUX_WRITER_DB_PORT"
|
||||
envDBUser = "MF_INFLUX_WRITER_DB_USER"
|
||||
envDBPass = "MF_INFLUX_WRITER_DB_PASS"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
NatsURL string
|
||||
Port string
|
||||
DBName string
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg, clientCfg := loadConfigs()
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
nc, err := nats.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
client, err := influxdata.NewHTTPClient(clientCfg)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create InfluxDB client: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
repo, err := influxdb.New(client, cfg.DBName)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create InfluxDB writer: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
counter, latency := makeMetrics()
|
||||
repo = writers.LoggingMiddleware(repo, logger)
|
||||
repo = writers.MetricsMiddleware(repo, counter, latency)
|
||||
if err := writers.Start(name, nc, logger, repo); err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to start message writer: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
errs := make(chan error, 2)
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
go startHTTPService(cfg.Port, logger, errs)
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("InfluxDB writer service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfigs() (config, influxdata.HTTPConfig) {
|
||||
cfg := config{
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
DBName: mainflux.Env(envDBName, defDBName),
|
||||
DBHost: mainflux.Env(envDBHost, defDBHost),
|
||||
DBPort: mainflux.Env(envDBPort, defDBPort),
|
||||
DBUser: mainflux.Env(envDBUser, defDBUser),
|
||||
DBPass: mainflux.Env(envDBPass, defDBPass),
|
||||
}
|
||||
|
||||
clientCfg := influxdata.HTTPConfig{
|
||||
Addr: fmt.Sprintf("%s%s:%s", prefix, cfg.DBHost, cfg.DBPort),
|
||||
Username: cfg.DBUser,
|
||||
Password: cfg.DBPass,
|
||||
}
|
||||
|
||||
return cfg, clientCfg
|
||||
}
|
||||
|
||||
func makeMetrics() (*kitprometheus.Counter, *kitprometheus.Summary) {
|
||||
counter := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "influxdb",
|
||||
Subsystem: "message_writer",
|
||||
Name: "request_count",
|
||||
Help: "Number of database inserts.",
|
||||
}, []string{"method"})
|
||||
|
||||
latency := kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "influxdb",
|
||||
Subsystem: "message_writer",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of inserts in microseconds.",
|
||||
}, []string{"method"})
|
||||
|
||||
return counter, latency
|
||||
}
|
||||
|
||||
func startHTTPService(port string, logger log.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
logger.Info(fmt.Sprintf("Influxdb writer service started, exposed port %s", p))
|
||||
errs <- http.ListenAndServe(p, influxdb.MakeHandler())
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/writers"
|
||||
mongodb "github.com/mainflux/mainflux/writers/mongodb"
|
||||
"github.com/mongodb/mongo-go-driver/mongo"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
name = "mongodb-writer"
|
||||
defNatsURL = nats.DefaultURL
|
||||
defPort = "8180"
|
||||
defDBName = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "27017"
|
||||
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envPort = "MF_MONGO_WRITER_PORT"
|
||||
envDBName = "MF_MONGO_WRITER_DB_NAME"
|
||||
envDBHost = "MF_MONGO_WRITER_DB_HOST"
|
||||
envDBPort = "MF_MONGO_WRITER_DB_PORT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
NatsURL string
|
||||
Port string
|
||||
DBName string
|
||||
DBHost string
|
||||
DBPort string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfigs()
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
nc, err := nats.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
client, err := mongo.Connect(context.Background(), fmt.Sprintf("mongodb://%s:%s", cfg.DBHost, cfg.DBPort), nil)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to database: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db := client.Database(cfg.DBName)
|
||||
repo, err := mongodb.New(db)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create MongoDB writer: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
counter, latency := makeMetrics()
|
||||
repo = writers.LoggingMiddleware(repo, logger)
|
||||
repo = writers.MetricsMiddleware(repo, counter, latency)
|
||||
if err := writers.Start(name, nc, logger, repo); err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to start message writer: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
errs := make(chan error, 2)
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
go startHTTPService(cfg.Port, logger, errs)
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("MongoDB writer service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfigs() config {
|
||||
return config{
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
DBName: mainflux.Env(envDBName, defDBName),
|
||||
DBHost: mainflux.Env(envDBHost, defDBHost),
|
||||
DBPort: mainflux.Env(envDBPort, defDBPort),
|
||||
}
|
||||
}
|
||||
|
||||
func makeMetrics() (*kitprometheus.Counter, *kitprometheus.Summary) {
|
||||
counter := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "mongodb",
|
||||
Subsystem: "message_writer",
|
||||
Name: "request_count",
|
||||
Help: "Number of database inserts.",
|
||||
}, []string{"method"})
|
||||
|
||||
latency := kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "mongodb",
|
||||
Subsystem: "message_writer",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of inserts in microseconds.",
|
||||
}, []string{"method"})
|
||||
|
||||
return counter, latency
|
||||
}
|
||||
|
||||
func startHTTPService(port string, logger log.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
logger.Info(fmt.Sprintf("Mongodb writer service started, exposed port %s", p))
|
||||
errs <- http.ListenAndServe(p, mongodb.MakeHandler())
|
||||
}
|
||||
@@ -11,14 +11,13 @@ import (
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"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"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
"github.com/mainflux/mainflux/things/api"
|
||||
grpcapi "github.com/mainflux/mainflux/things/api/grpc"
|
||||
httpapi "github.com/mainflux/mainflux/things/api/http"
|
||||
"github.com/mainflux/mainflux/things/postgres"
|
||||
"github.com/mainflux/mainflux/things/uuid"
|
||||
usersapi "github.com/mainflux/mainflux/users/api/grpc"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
@@ -29,20 +28,18 @@ const (
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "clients"
|
||||
defDBName = "things"
|
||||
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"
|
||||
envDBHost = "MF_THINGS_DB_HOST"
|
||||
envDBPort = "MF_THINGS_DB_PORT"
|
||||
envDBUser = "MF_THINGS_DB_USER"
|
||||
envDBPass = "MF_THINGS_DB_PASS"
|
||||
envDBName = "MF_THINGS_DB"
|
||||
envHTTPPort = "MF_THINGS_HTTP_PORT"
|
||||
envGRPCPort = "MF_THINGS_GRPC_PORT"
|
||||
envUsersURL = "MF_USERS_URL"
|
||||
envSecret = "MF_CLIENTS_SECRET"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
@@ -54,7 +51,6 @@ type config struct {
|
||||
HTTPPort string
|
||||
GRPCPort string
|
||||
UsersURL string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -68,7 +64,7 @@ func main() {
|
||||
conn := connectToUsersService(cfg.UsersURL, logger)
|
||||
defer conn.Close()
|
||||
|
||||
svc := newService(conn, db, cfg.Secret, logger)
|
||||
svc := newService(conn, db, logger)
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(svc, cfg.HTTPPort, logger, errs)
|
||||
@@ -81,7 +77,7 @@ func main() {
|
||||
}()
|
||||
|
||||
err := <-errs
|
||||
logger.Error(fmt.Sprintf("Clients service terminated: %s", err))
|
||||
logger.Error(fmt.Sprintf("Things service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
@@ -94,7 +90,6 @@ func loadConfig() config {
|
||||
HTTPPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
GRPCPort: mainflux.Env(envGRPCPort, defGRPCPort),
|
||||
UsersURL: mainflux.Env(envUsersURL, defUsersURL),
|
||||
Secret: mainflux.Env(envSecret, defSecret),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,25 +111,24 @@ func connectToUsersService(usersAddr string, logger log.Logger) *grpc.ClientConn
|
||||
return conn
|
||||
}
|
||||
|
||||
func newService(conn *grpc.ClientConn, db *sql.DB, secret string, logger log.Logger) clients.Service {
|
||||
func newService(conn *grpc.ClientConn, db *sql.DB, logger log.Logger) things.Service {
|
||||
users := usersapi.NewClient(conn)
|
||||
clientsRepo := postgres.NewClientRepository(db, logger)
|
||||
thingsRepo := postgres.NewThingRepository(db, logger)
|
||||
channelsRepo := postgres.NewChannelRepository(db, logger)
|
||||
hasher := bcrypt.New()
|
||||
idp := jwt.New(secret)
|
||||
idp := uuid.New()
|
||||
|
||||
svc := clients.New(users, clientsRepo, channelsRepo, hasher, idp)
|
||||
svc := things.New(users, thingsRepo, channelsRepo, idp)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "clients",
|
||||
Namespace: "things",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "clients",
|
||||
Namespace: "things",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
@@ -143,20 +137,20 @@ func newService(conn *grpc.ClientConn, db *sql.DB, secret string, logger log.Log
|
||||
return svc
|
||||
}
|
||||
|
||||
func startHTTPServer(svc clients.Service, port string, logger log.Logger, errs chan error) {
|
||||
func startHTTPServer(svc things.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))
|
||||
logger.Info(fmt.Sprintf("Things 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) {
|
||||
func startGRPCServer(svc things.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))
|
||||
mainflux.RegisterThingsServiceServer(server, grpcapi.NewServer(svc))
|
||||
logger.Info(fmt.Sprintf("Things gRPC service started, exposed port %s", port))
|
||||
errs <- server.Serve(listener)
|
||||
}
|
||||
+15
-15
@@ -9,8 +9,8 @@ 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"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/grpc"
|
||||
adapter "github.com/mainflux/mainflux/ws"
|
||||
"github.com/mainflux/mainflux/ws/api"
|
||||
"github.com/mainflux/mainflux/ws/nats"
|
||||
@@ -20,25 +20,25 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defPort = "8180"
|
||||
defNatsURL = broker.DefaultURL
|
||||
defClientsURL = "localhost:8181"
|
||||
envPort = "MF_WS_ADAPTER_PORT"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envClientsURL = "MF_CLIENTS_URL"
|
||||
defPort = "8180"
|
||||
defNatsURL = broker.DefaultURL
|
||||
defThingsURL = "localhost:8181"
|
||||
envPort = "MF_WS_ADAPTER_PORT"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envThingsURL = "MF_THINGS_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ClientsURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
ThingsURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
ThingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
@@ -50,14 +50,14 @@ func main() {
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
|
||||
conn, err := grpc.Dial(cfg.ThingsURL, 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)
|
||||
cc := thingsapi.NewClient(conn)
|
||||
pubsub := nats.New(nc)
|
||||
svc := adapter.New(pubsub)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# Mainflux CoAP Adapter
|
||||
|
||||
Mainflux CoAP adapter provides an [CoAP](http://coap.technology/) API for sending messages through the
|
||||
platform.
|
||||
|
||||
## 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_COAP_ADAPTER_PORT | adapter listening port | `5683` |
|
||||
| MF_NATS_URL | NATS instance URL | `nats://localhost:4222` |
|
||||
| MF_CLIENTS_URL | clients service URL | `http://localhost:8181` |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service 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:
|
||||
adapter:
|
||||
image: mainflux/coap-adapter:[version]
|
||||
container_name: [instance name]
|
||||
ports:
|
||||
- [host machine port]:[configured port]
|
||||
environment:
|
||||
MF_COAP_ADAPTER_PORT: [Service HTTP port]
|
||||
MF_NATS_URL: [NATS instance URL]
|
||||
MF_CLIENTS_URL: [Manager service URL]
|
||||
```
|
||||
|
||||
Running this service outside of container requires working instance of the NATS service.
|
||||
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 http
|
||||
make coap
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_MANAGER_URL=[Manager service URL] MF_NATS_URL=[NATS instance URL] MF_COAP_ADAPTER_PORT=[Service HTTP port] $GOBIN/mainflux-coap
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Since CoAP protocol does not support `Authorization` header (option), in order to send CoAP messages,
|
||||
client valid key must be present in `Uri-Query` option.
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
// Package coap contains the domain concept definitions needed to support
|
||||
// Mainflux coap adapter service functionality. All constant values are taken
|
||||
// from RFC, and could be adjusted based on specific use case.
|
||||
package coap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap/nats"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const (
|
||||
responseBackoffMultiplier = 1.5
|
||||
|
||||
// AckTimeout is the amount of time to wait for a response.
|
||||
AckTimeout = int(2 * time.Second)
|
||||
|
||||
// MaxRetransmit is the maximum number of times a message will be retransmitted.
|
||||
MaxRetransmit = 4
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrFailedMessagePublish indicates that message publishing failed.
|
||||
ErrFailedMessagePublish = errors.New("failed to publish message")
|
||||
|
||||
// ErrFailedSubscription indicates that client couldn't subscribe to specified channel.
|
||||
ErrFailedSubscription = errors.New("failed to subscribe to a channel")
|
||||
|
||||
// ErrFailedConnection indicates that service couldn't connect to message broker.
|
||||
ErrFailedConnection = errors.New("failed to connect to message broker")
|
||||
|
||||
// extracted to avoid recomputation
|
||||
maxTimeout = int(float64(AckTimeout) * ((math.Pow(2, float64(MaxRetransmit))) - 1) * responseBackoffMultiplier)
|
||||
)
|
||||
|
||||
// Service specifies coap service API.
|
||||
type Service interface {
|
||||
mainflux.MessagePublisher
|
||||
|
||||
// Subscribes to channel with specified id and adds subscription to
|
||||
// service map of subscriptions under given ID.
|
||||
Subscribe(string, string, nats.Channel) error
|
||||
|
||||
// Unsubscribe method is used to stop observing resource.
|
||||
Unsubscribe(string)
|
||||
|
||||
// SetTimeout sets timeout to wait CONF messages.
|
||||
SetTimeout(string, *time.Timer, int) (chan bool, error)
|
||||
|
||||
// RemoveTimeout removes timeout when ACK message is received from client
|
||||
// if timeout existed.
|
||||
RemoveTimeout(string)
|
||||
}
|
||||
|
||||
var _ Service = (*adapterService)(nil)
|
||||
|
||||
type adapterService struct {
|
||||
pubsub nats.Service
|
||||
subs map[string]nats.Channel
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// New instantiates the CoAP adapter implementation.
|
||||
func New(pubsub nats.Service) Service {
|
||||
return &adapterService{
|
||||
pubsub: pubsub,
|
||||
subs: make(map[string]nats.Channel),
|
||||
mu: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *adapterService) get(clientID string) (nats.Channel, bool) {
|
||||
svc.mu.Lock()
|
||||
obs, ok := svc.subs[clientID]
|
||||
svc.mu.Unlock()
|
||||
return obs, ok
|
||||
}
|
||||
|
||||
func (svc *adapterService) put(clientID string, obs nats.Channel) {
|
||||
svc.mu.Lock()
|
||||
svc.subs[clientID] = obs
|
||||
svc.mu.Unlock()
|
||||
}
|
||||
|
||||
func (svc *adapterService) remove(clientID string) {
|
||||
svc.mu.Lock()
|
||||
obs, ok := svc.subs[clientID]
|
||||
if ok {
|
||||
obs.Closed <- true
|
||||
delete(svc.subs, clientID)
|
||||
}
|
||||
svc.mu.Unlock()
|
||||
}
|
||||
|
||||
func (svc *adapterService) Publish(msg mainflux.RawMessage) error {
|
||||
if err := svc.pubsub.Publish(msg); err != nil {
|
||||
switch err {
|
||||
case broker.ErrConnectionClosed, broker.ErrInvalidConnection:
|
||||
return ErrFailedConnection
|
||||
default:
|
||||
return ErrFailedMessagePublish
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *adapterService) Subscribe(chanID, clientID string, ch nats.Channel) error {
|
||||
// Remove entry if already exists.
|
||||
svc.Unsubscribe(clientID)
|
||||
if err := svc.pubsub.Subscribe(chanID, ch); err != nil {
|
||||
return ErrFailedSubscription
|
||||
}
|
||||
svc.put(clientID, ch)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *adapterService) Unsubscribe(clientID string) {
|
||||
svc.remove(clientID)
|
||||
}
|
||||
|
||||
func (svc *adapterService) SetTimeout(clientID string, timer *time.Timer, duration int) (chan bool, error) {
|
||||
sub, ok := svc.get(clientID)
|
||||
if !ok {
|
||||
return nil, errors.New("observer entry not found")
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-sub.Timer:
|
||||
timer.Stop()
|
||||
if ok {
|
||||
sub.Notify <- false
|
||||
}
|
||||
return
|
||||
case <-timer.C:
|
||||
duration *= 2
|
||||
if duration >= maxTimeout {
|
||||
timer.Stop()
|
||||
sub.Notify <- false
|
||||
svc.Unsubscribe(clientID)
|
||||
return
|
||||
}
|
||||
timer.Reset(time.Duration(duration))
|
||||
sub.Notify <- true
|
||||
}
|
||||
}
|
||||
}()
|
||||
return sub.Notify, nil
|
||||
}
|
||||
|
||||
func (svc *adapterService) RemoveTimeout(clientID string) {
|
||||
if sub, ok := svc.get(clientID); ok {
|
||||
sub.Timer <- false
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package api contains API-related concerns: endpoint definitions, middlewares
|
||||
// and all resource representations.
|
||||
package api
|
||||
@@ -1,66 +0,0 @@
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"github.com/mainflux/mainflux/coap/nats"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ coap.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc coap.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the adapter.
|
||||
func LoggingMiddleware(svc coap.Service, logger log.Logger) coap.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Publish(msg mainflux.RawMessage) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method publish to channel %s took %s to complete", msg.Channel, 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.Publish(msg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Subscribe(chanID, clientID string, channel nats.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method subscribe to channel %s took %s to complete", chanID, 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.Subscribe(chanID, clientID, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) SetTimeout(clientID string, timer *time.Timer, duration int) (chan bool, error) {
|
||||
return lm.svc.SetTimeout(clientID, timer, duration)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveTimeout(clientID string) {
|
||||
lm.svc.RemoveTimeout(clientID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Unsubscribe(clientID string) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Info(fmt.Sprintf("Method unsubscribe for client %s took %s to complete", clientID, time.Since(begin)))
|
||||
}(time.Now())
|
||||
lm.svc.Unsubscribe(clientID)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"github.com/mainflux/mainflux/coap/nats"
|
||||
)
|
||||
|
||||
var _ coap.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc coap.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments adapter by tracking request count and latency.
|
||||
func MetricsMiddleware(svc coap.Service, counter metrics.Counter, latency metrics.Histogram) coap.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Publish(msg mainflux.RawMessage) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "publish").Add(1)
|
||||
mm.latency.With("method", "publish").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Publish(msg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Subscribe(chanID, clientID string, channel nats.Channel) error {
|
||||
return mm.svc.Subscribe(chanID, clientID, channel)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) SetTimeout(clientID string, timer *time.Timer, duration int) (chan bool, error) {
|
||||
return mm.svc.SetTimeout(clientID, timer, duration)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) RemoveTimeout(clientID string) {
|
||||
mm.svc.RemoveTimeout(clientID)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Unsubscribe(clientID string) {
|
||||
mm.svc.Unsubscribe(clientID)
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
mux "github.com/dereulenspiegel/coap-mux"
|
||||
gocoap "github.com/dustin/go-coap"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
chanID = "id"
|
||||
keyHeader = "key"
|
||||
)
|
||||
|
||||
func authKey(opt interface{}) (string, error) {
|
||||
val, ok := opt.(string)
|
||||
if !ok {
|
||||
return "", errBadRequest
|
||||
}
|
||||
arr := strings.Split(val, "=")
|
||||
if len(arr) != 2 || strings.ToLower(arr[0]) != keyHeader {
|
||||
return "", errBadOption
|
||||
}
|
||||
return arr[1], nil
|
||||
}
|
||||
|
||||
func authorize(msg *gocoap.Message, res *gocoap.Message, cid string) (publisher *mainflux.Identity, err error) {
|
||||
if !govalidator.IsUUID(cid) {
|
||||
res.Code = gocoap.NotFound
|
||||
return
|
||||
}
|
||||
|
||||
// Device Key is passed as Uri-Query parameter, which option ID is 15 (0xf).
|
||||
key, err := authKey(msg.Option(gocoap.URIQuery))
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errBadOption:
|
||||
res.Code = gocoap.BadOption
|
||||
case errBadRequest:
|
||||
res.Code = gocoap.BadRequest
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
publisher, err = auth.CanAccess(ctx, &mainflux.AccessReq{Token: key, ChanID: cid})
|
||||
|
||||
if err != nil {
|
||||
e, ok := status.FromError(err)
|
||||
if ok {
|
||||
switch e.Code() {
|
||||
case codes.PermissionDenied:
|
||||
res.Code = gocoap.Forbidden
|
||||
default:
|
||||
res.Code = gocoap.ServiceUnavailable
|
||||
}
|
||||
return
|
||||
}
|
||||
res.Code = gocoap.InternalServerError
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func serve(svc coap.Service, conn *net.UDPConn, data []byte, addr *net.UDPAddr, rh gocoap.Handler) {
|
||||
msg, err := gocoap.ParseMessage(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res := &gocoap.Message{
|
||||
Type: gocoap.NonConfirmable,
|
||||
Code: gocoap.Content,
|
||||
MessageID: msg.MessageID,
|
||||
Token: msg.Token,
|
||||
Payload: []byte{},
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case gocoap.Reset:
|
||||
if len(msg.Payload) != 0 {
|
||||
res.Code = gocoap.BadRequest
|
||||
break
|
||||
}
|
||||
cid := mux.Var(&msg, chanID)
|
||||
res.Type = gocoap.Acknowledgement
|
||||
publisher, err := authorize(&msg, res, cid)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
|
||||
svc.RemoveTimeout(id)
|
||||
svc.Unsubscribe(id)
|
||||
case gocoap.Acknowledgement:
|
||||
cid := mux.Var(&msg, chanID)
|
||||
res.Type = gocoap.Acknowledgement
|
||||
publisher, err := authorize(&msg, res, cid)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
|
||||
svc.RemoveTimeout(id)
|
||||
default:
|
||||
res = rh.ServeCOAP(conn, addr, &msg)
|
||||
}
|
||||
if res != nil && msg.IsConfirmable() {
|
||||
gocoap.Transmit(conn, addr, *res)
|
||||
}
|
||||
}
|
||||
|
||||
// ListenAndServe binds to the given address and serve requests forever.
|
||||
func ListenAndServe(svc coap.Service, csc mainflux.ClientsServiceClient, addr string) error {
|
||||
|
||||
handler := makeHandler(addr, svc)
|
||||
auth = csc
|
||||
uaddr, err := net.ResolveUDPAddr(network, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := net.ListenUDP(network, uaddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := make([]byte, maxPktLen)
|
||||
for {
|
||||
nr, addr, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
if neterr, ok := err.(net.Error); ok && (neterr.Temporary() || neterr.Timeout()) {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
tmp := make([]byte, nr)
|
||||
copy(tmp, buf)
|
||||
go serve(svc, conn, tmp, addr, handler)
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"github.com/mainflux/mainflux/coap/nats"
|
||||
|
||||
"math/rand"
|
||||
|
||||
mux "github.com/dereulenspiegel/coap-mux"
|
||||
gocoap "github.com/dustin/go-coap"
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
var (
|
||||
errBadRequest = errors.New("bad request")
|
||||
errBadOption = errors.New("bad option")
|
||||
auth mainflux.ClientsServiceClient
|
||||
)
|
||||
|
||||
const (
|
||||
maxPktLen = 1500
|
||||
network = "udp"
|
||||
protocol = "coap"
|
||||
)
|
||||
|
||||
const (
|
||||
// Approximately number of supported requests per second
|
||||
timestamp = int64(time.Millisecond) * 31
|
||||
)
|
||||
|
||||
type handler func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message
|
||||
|
||||
// NotFoundHandler handles erroneously formed requests.
|
||||
func NotFoundHandler(l *net.UDPConn, a *net.UDPAddr, m *gocoap.Message) *gocoap.Message {
|
||||
if m.IsConfirmable() {
|
||||
return &gocoap.Message{
|
||||
Type: gocoap.Acknowledgement,
|
||||
Code: gocoap.NotFound,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func version(port string) {
|
||||
b := bone.New()
|
||||
b.GetFunc("/version", mainflux.Version("CoAP"))
|
||||
http.ListenAndServe(port, b)
|
||||
}
|
||||
|
||||
func makeHandler(port string, svc coap.Service) gocoap.Handler {
|
||||
go version(port)
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.Handle("/channels/{id}/messages", gocoap.FuncHandler(receive(svc))).Methods(gocoap.POST)
|
||||
r.Handle("/channels/{id}/messages", gocoap.FuncHandler(observe(svc))).Methods(gocoap.GET)
|
||||
r.NotFoundHandler = gocoap.FuncHandler(NotFoundHandler)
|
||||
return r
|
||||
}
|
||||
|
||||
func receive(svc coap.Service) handler {
|
||||
return func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message {
|
||||
var res *gocoap.Message
|
||||
if msg.IsConfirmable() {
|
||||
res = &gocoap.Message{
|
||||
Type: gocoap.Acknowledgement,
|
||||
Code: gocoap.Content,
|
||||
MessageID: msg.MessageID,
|
||||
Token: msg.Token,
|
||||
Payload: []byte{},
|
||||
}
|
||||
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
|
||||
}
|
||||
|
||||
if len(msg.Payload) == 0 && msg.IsConfirmable() {
|
||||
res.Code = gocoap.BadRequest
|
||||
return res
|
||||
}
|
||||
|
||||
cid := mux.Var(msg, "id")
|
||||
publisher, err := authorize(msg, res, cid)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
|
||||
rawMsg := mainflux.RawMessage{
|
||||
Channel: cid,
|
||||
Publisher: publisher.GetValue(),
|
||||
Protocol: protocol,
|
||||
Payload: msg.Payload,
|
||||
}
|
||||
|
||||
if err := svc.Publish(rawMsg); err != nil {
|
||||
res.Code = gocoap.InternalServerError
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
func observe(svc coap.Service) handler {
|
||||
return func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message {
|
||||
var res *gocoap.Message
|
||||
if msg.IsConfirmable() {
|
||||
res = &gocoap.Message{
|
||||
Type: gocoap.Acknowledgement,
|
||||
Code: gocoap.Content,
|
||||
MessageID: msg.MessageID,
|
||||
Token: msg.Token,
|
||||
Payload: []byte{},
|
||||
}
|
||||
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
|
||||
}
|
||||
|
||||
cid := mux.Var(msg, "id")
|
||||
publisher, err := authorize(msg, res, cid)
|
||||
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
|
||||
if value, ok := msg.Option(gocoap.Observe).(uint32); ok && value == 1 {
|
||||
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
|
||||
svc.Unsubscribe(id)
|
||||
}
|
||||
|
||||
if value, ok := msg.Option(gocoap.Observe).(uint32); ok && value == 0 {
|
||||
ch := nats.Channel{
|
||||
Messages: make(chan mainflux.RawMessage),
|
||||
Closed: make(chan bool),
|
||||
Timer: make(chan bool),
|
||||
Notify: make(chan bool),
|
||||
}
|
||||
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
|
||||
if err := svc.Subscribe(cid, id, ch); err != nil {
|
||||
res.Code = gocoap.InternalServerError
|
||||
return res
|
||||
}
|
||||
go handleSub(svc, id, conn, addr, msg, ch)
|
||||
res.AddOption(gocoap.Observe, 0)
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(svc coap.Service, id string, conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) error {
|
||||
buff := new(bytes.Buffer)
|
||||
now := time.Now().UnixNano() / timestamp
|
||||
if err := binary.Write(buff, binary.BigEndian, now); err != nil {
|
||||
return err
|
||||
}
|
||||
observeVal := buff.Bytes()
|
||||
msg.SetOption(gocoap.Observe, observeVal[len(observeVal)-3:])
|
||||
if msg.IsConfirmable() {
|
||||
timer := time.NewTimer(time.Duration(coap.AckTimeout))
|
||||
ch, err := svc.SetTimeout(id, timer, coap.AckTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go sendConfirmable(conn, addr, msg, ch)
|
||||
return nil
|
||||
}
|
||||
return gocoap.Transmit(conn, addr, *msg)
|
||||
}
|
||||
|
||||
func sendConfirmable(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message, ch chan bool) {
|
||||
msg.SetOption(gocoap.MaxRetransmit, coap.MaxRetransmit)
|
||||
// Try to transmit MAX_RETRANSMITION times; every attempt duplicates timeout between transmission.
|
||||
for i := 0; i < coap.MaxRetransmit; i++ {
|
||||
if err := gocoap.Transmit(conn, addr, *msg); err != nil {
|
||||
return
|
||||
}
|
||||
state, ok := <-ch
|
||||
if !state || !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func handleSub(svc coap.Service, id string, conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message, ch nats.Channel) {
|
||||
// According to RFC (https://tools.ietf.org/html/rfc7641#page-18), CON message must be sent at least every
|
||||
// 24 hours. Since 24 hours is too long for our purposes, we use 12.
|
||||
ticker := time.NewTicker(12 * time.Hour)
|
||||
res := &gocoap.Message{
|
||||
Type: gocoap.NonConfirmable,
|
||||
Code: gocoap.Content,
|
||||
MessageID: msg.MessageID,
|
||||
Token: msg.Token,
|
||||
Payload: []byte{},
|
||||
}
|
||||
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
|
||||
res.SetOption(gocoap.LocationPath, msg.Path())
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ticker.Stop()
|
||||
res.Type = gocoap.Confirmable
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
if err := sendMessage(svc, id, conn, addr, res); err != nil {
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
case rawMsg, ok := <-ch.Messages:
|
||||
if !ok {
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
res.Type = gocoap.NonConfirmable
|
||||
res.Payload = rawMsg.Payload
|
||||
if err := sendMessage(svc, id, conn, addr, res); err != nil {
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// Package nats contains NATS-specific message repository implementation.
|
||||
package nats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
|
||||
|
||||
const prefix = "channel"
|
||||
|
||||
type natsPublisher struct {
|
||||
nc *broker.Conn
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// New instantiates NATS message pubsub.
|
||||
func New(nc *broker.Conn, l log.Logger) Service {
|
||||
return &natsPublisher{nc, l}
|
||||
}
|
||||
|
||||
func (pubsub *natsPublisher) Publish(msg mainflux.RawMessage) error {
|
||||
data, err := proto.Marshal(&msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return pubsub.nc.Publish(fmt.Sprintf("%s.%s", prefix, msg.Channel), data)
|
||||
}
|
||||
|
||||
func (pubsub *natsPublisher) Subscribe(chanID string, channel Channel) error {
|
||||
sub, err := pubsub.nc.Subscribe(fmt.Sprintf("%s.%s", prefix, chanID), func(msg *broker.Msg) {
|
||||
if msg == nil {
|
||||
return
|
||||
}
|
||||
var rawMsg mainflux.RawMessage
|
||||
if err := proto.Unmarshal(msg.Data, &rawMsg); err != nil {
|
||||
return
|
||||
}
|
||||
channel.Messages <- rawMsg
|
||||
})
|
||||
|
||||
go func() {
|
||||
<-channel.Closed
|
||||
sub.Unsubscribe()
|
||||
channel.Close()
|
||||
}()
|
||||
return err
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package nats
|
||||
|
||||
import "github.com/mainflux/mainflux"
|
||||
|
||||
// Service specifies NATS service API.
|
||||
type Service interface {
|
||||
mainflux.MessagePublisher
|
||||
// Subscribe is used to subscribe to channel with specified id.
|
||||
Subscribe(string, Channel) error
|
||||
}
|
||||
|
||||
// Channel is used for receiving and sending messages.
|
||||
type Channel struct {
|
||||
Messages chan mainflux.RawMessage
|
||||
Closed chan bool
|
||||
Timer chan bool
|
||||
Notify chan bool
|
||||
}
|
||||
|
||||
// Close channel and stop message transfer.
|
||||
func (channel Channel) Close() {
|
||||
close(channel.Messages)
|
||||
close(channel.Closed)
|
||||
close(channel.Timer)
|
||||
close(channel.Notify)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
# Stage 0, based on Node.js, to build and compile Angular
|
||||
FROM node:8.6 as node
|
||||
FROM node:9.5 as node
|
||||
WORKDIR /app
|
||||
COPY package.json /app/
|
||||
RUN npm install
|
||||
@@ -8,6 +8,6 @@ ARG env=prod
|
||||
RUN npm run build -- --prod --environment $env
|
||||
|
||||
# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
|
||||
FROM nginx:1.13
|
||||
FROM nginx:1.13-alpine
|
||||
COPY --from=node /app/dist/ /usr/share/nginx/html
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen 3000;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
|
||||
@@ -6,13 +6,13 @@ import { ProtectedGuard, PublicGuard } from 'ngx-auth';
|
||||
import { LoginComponent } from './components/auth/login/login.component';
|
||||
import { SignupComponent } from './components/auth/signup/signup.component';
|
||||
import { ChannelsComponent } from './components/channels/channels.component';
|
||||
import { ClientsComponent } from './components/clients/clients.component';
|
||||
import { ThingsComponent } from './components/things/things.component';
|
||||
|
||||
const routes: Route[] = [
|
||||
{ path: '', redirectTo: 'clients', pathMatch: 'full'},
|
||||
{ path: '', redirectTo: 'things', pathMatch: 'full'},
|
||||
{ path: 'login', component: LoginComponent, canActivate: [PublicGuard]},
|
||||
{ path: 'signup', component: SignupComponent, canActivate: [PublicGuard]},
|
||||
{ path: 'clients', component: ClientsComponent, canActivate: [ProtectedGuard]},
|
||||
{ path: 'things', component: ThingsComponent, canActivate: [ProtectedGuard]},
|
||||
{ path: 'channels', component: ChannelsComponent, canActivate: [ProtectedGuard]}
|
||||
];
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<mat-toolbar color="primary" class="mat-elevation-z6">
|
||||
<h1 fxFlex>Mainflux</h1>
|
||||
<ng-container *ngIf="authStore.isAuthenticated">
|
||||
<a [routerLink]="['/clients']" mat-button>Clients</a>
|
||||
<a [routerLink]="['/things']" mat-button>Things</a>
|
||||
<a [routerLink]="['/channels']" mat-button>Channels</a>
|
||||
<a (click)="logout()" mat-button>Logout</a>
|
||||
</ng-container>
|
||||
|
||||
|
||||
</mat-toolbar>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { MaterialModule } from './core/material/material.module';
|
||||
import { AuthenticationService } from './core/services/auth/authentication.service';
|
||||
import { TokenStorage } from './core/services/auth/token-storage.service';
|
||||
import { ChannelsService } from './core/services/channels/channels.service';
|
||||
import { ClientsService } from './core/services/clients/clients.service';
|
||||
import { ThingsService } from './core/services/things/things.service';
|
||||
import { UiStore } from './core/store/ui.store';
|
||||
import { AuthStore } from './core/store/auth.store';
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('AppComponent', () => {
|
||||
AuthStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ClientsService,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
@@ -2,6 +2,7 @@ import './rxjs-extensions.ts';
|
||||
import 'hammerjs';
|
||||
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
@@ -14,21 +15,21 @@ import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { LoginComponent } from './components/auth/login/login.component';
|
||||
import { SignupComponent } from './components/auth/signup/signup.component';
|
||||
import { AddChannelDialogComponent } from './components/channels/add-channel-dialog/add-channel-dialog.component';
|
||||
import { ChannelDialogComponent } from './components/channels/channel-dialog/channel-dialog.component';
|
||||
import { ChannelsComponent } from './components/channels/channels.component';
|
||||
import { AddClientDialogComponent } from './components/clients/add-client-dialog/add-client-dialog.component';
|
||||
import { ClientsComponent } from './components/clients/clients.component';
|
||||
import { ThingDialogComponent } from './components/things/thing-dialog/thing-dialog.component';
|
||||
import { ThingsComponent } from './components/things/things.component';
|
||||
import { ConfirmationDialogComponent } from './components/shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { MaterialModule } from './core/material/material.module';
|
||||
import { AuthenticationService } from './core/services/auth/authentication.service';
|
||||
import { TokenStorage } from './core/services/auth/token-storage.service';
|
||||
import { ChannelsService } from './core/services/channels/channels.service';
|
||||
import { ClientsService } from './core/services/clients/clients.service';
|
||||
import { ThingsService } from './core/services/things/things.service';
|
||||
import { MockAuthService } from './core/services/mock-auth.service';
|
||||
import { MockChannelsService } from './core/services/mock-channels.service';
|
||||
import { MockClientsService } from './core/services/mock-clients.service';
|
||||
import { MockThingsService } from './core/services/mock-things.service';
|
||||
import { ChannelsStore } from './core/store/channels.store';
|
||||
import { ClientsStore } from './core/store/clients.store';
|
||||
import { ThingsStore } from './core/store/things.store';
|
||||
import { UiStore } from './core/store/ui.store';
|
||||
import { AuthStore } from './core/store/auth.store';
|
||||
import { UnauthorizedInterceptor } from './core/services/auth/unauthorized.interceptor';
|
||||
@@ -40,13 +41,13 @@ export function factory(authenticationService: AuthenticationService) {
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
ClientsComponent,
|
||||
ThingsComponent,
|
||||
ChannelsComponent,
|
||||
SignupComponent,
|
||||
LoginComponent,
|
||||
AddClientDialogComponent,
|
||||
ThingDialogComponent,
|
||||
ConfirmationDialogComponent,
|
||||
AddChannelDialogComponent,
|
||||
ChannelDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
AuthModule,
|
||||
@@ -61,13 +62,13 @@ export function factory(authenticationService: AuthenticationService) {
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
ClientsStore,
|
||||
ThingsStore,
|
||||
ChannelsStore,
|
||||
AuthStore,
|
||||
MockAuthService,
|
||||
MockClientsService,
|
||||
MockThingsService,
|
||||
MockChannelsService,
|
||||
ClientsService,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
TokenStorage,
|
||||
AuthenticationService,
|
||||
@@ -79,11 +80,12 @@ export function factory(authenticationService: AuthenticationService) {
|
||||
useFactory: factory
|
||||
},
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: UnauthorizedInterceptor, multi: true },
|
||||
{ provide: APP_BASE_HREF, useValue: '/app/' }
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
entryComponents: [
|
||||
AddClientDialogComponent,
|
||||
AddChannelDialogComponent,
|
||||
ThingDialogComponent,
|
||||
ChannelDialogComponent,
|
||||
ConfirmationDialogComponent
|
||||
]
|
||||
})
|
||||
|
||||
+3
-3
@@ -10,8 +10,8 @@
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="Connected clients" formControlName="connected" [compareWith]="compareFunction" multiple>
|
||||
<mat-option *ngFor="let client of clientsStore.clients" [value]="client">{{client.name}}</mat-option>
|
||||
<mat-select placeholder="Connected things" formControlName="connected" [compareWith]="compareFunction" multiple>
|
||||
<mat-option *ngFor="let thing of thingsStore.things" [value]="thing">{{thing.name}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@@ -19,4 +19,4 @@
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button mat-button mat-raised-button color="primary" type="submit" [disabled]="addChannelForm.invalid">Ok</button>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
+10
-10
@@ -9,18 +9,18 @@ import { MaterialModule } from '../../../core/material/material.module';
|
||||
import { AuthenticationService } from '../../../core/services/auth/authentication.service';
|
||||
import { TokenStorage } from '../../../core/services/auth/token-storage.service';
|
||||
import { ChannelsService } from '../../../core/services/channels/channels.service';
|
||||
import { ClientsService } from '../../../core/services/clients/clients.service';
|
||||
import { AddChannelDialogComponent } from './add-channel-dialog.component';
|
||||
import { ClientsStore } from '../../../core/store/clients.store';
|
||||
import { ThingsService } from '../../../core/services/things/things.service';
|
||||
import { ChannelDialogComponent } from './channel-dialog.component';
|
||||
import { ThingsStore } from '../../../core/store/things.store';
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
|
||||
describe('AddChannelDialogComponent', () => {
|
||||
let component: AddChannelDialogComponent;
|
||||
let fixture: ComponentFixture<AddChannelDialogComponent>;
|
||||
describe('ChannelDialogComponent', () => {
|
||||
let component: ChannelDialogComponent;
|
||||
let fixture: ComponentFixture<ChannelDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddChannelDialogComponent ],
|
||||
declarations: [ ChannelDialogComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
@@ -31,11 +31,11 @@ describe('AddChannelDialogComponent', () => {
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
ClientsStore,
|
||||
ThingsStore,
|
||||
UiStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ClientsService,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
@@ -45,7 +45,7 @@ describe('AddChannelDialogComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AddChannelDialogComponent);
|
||||
fixture = TestBed.createComponent(ChannelDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
+9
-9
@@ -3,24 +3,24 @@ import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
import { toJS } from 'mobx';
|
||||
|
||||
import { ClientsStore } from '../../../core/store/clients.store';
|
||||
import { Channel, Client } from '../../../core/store/models';
|
||||
import { ThingsStore } from '../../../core/store/things.store';
|
||||
import { Channel, Thing } from '../../../core/store/models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-channel-dialog',
|
||||
templateUrl: './add-channel-dialog.component.html',
|
||||
styleUrls: ['./add-channel-dialog.component.scss']
|
||||
selector: 'app-channel-dialog',
|
||||
templateUrl: './channel-dialog.component.html',
|
||||
styleUrls: ['./channel-dialog.component.scss']
|
||||
})
|
||||
export class AddChannelDialogComponent implements OnInit {
|
||||
export class ChannelDialogComponent implements OnInit {
|
||||
addChannelForm: FormGroup;
|
||||
@Output() submit: EventEmitter<Channel> = new EventEmitter<Channel>();
|
||||
editMode: boolean;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialogRef: MatDialogRef<AddChannelDialogComponent>,
|
||||
private dialogRef: MatDialogRef<ChannelDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: Channel,
|
||||
public clientsStore: ClientsStore,
|
||||
public thingsStore: ThingsStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
@@ -46,7 +46,7 @@ export class AddChannelDialogComponent implements OnInit {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
compareFunction(obj1: Client, obj2: Client) {
|
||||
compareFunction(obj1: Thing, obj2: Thing) {
|
||||
return obj1.id === obj2.id;
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,9 @@ import { MaterialModule } from '../../core/material/material.module';
|
||||
import { AuthenticationService } from '../../core/services/auth/authentication.service';
|
||||
import { TokenStorage } from '../../core/services/auth/token-storage.service';
|
||||
import { ChannelsService } from '../../core/services/channels/channels.service';
|
||||
import { ClientsService } from '../../core/services/clients/clients.service';
|
||||
import { ThingsService } from '../../core/services/things/things.service';
|
||||
import { ChannelsComponent } from './channels.component';
|
||||
import { ClientsStore } from '../../core/store/clients.store';
|
||||
import { ThingsStore } from '../../core/store/things.store';
|
||||
import { UiStore } from '../../core/store/ui.store';
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
|
||||
@@ -33,9 +33,9 @@ describe('ChannelsComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: ClientsStore,
|
||||
provide: ThingsStore,
|
||||
useClass: class {
|
||||
getClients = jasmine.createSpy('getClients');
|
||||
getThings = jasmine.createSpy('getThings');
|
||||
}
|
||||
},
|
||||
UiStore,
|
||||
@@ -47,7 +47,7 @@ describe('ChannelsComponent', () => {
|
||||
},
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ClientsService,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
|
||||
@@ -3,10 +3,10 @@ import { MatDialog } from '@angular/material';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
import { ClientsStore } from '../../core/store/clients.store';
|
||||
import { ThingsStore } from '../../core/store/things.store';
|
||||
import { Channel } from '../../core/store/models';
|
||||
import { ConfirmationDialogComponent } from '../shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { AddChannelDialogComponent } from './add-channel-dialog/add-channel-dialog.component';
|
||||
import { ChannelDialogComponent } from './channel-dialog/channel-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-channels',
|
||||
@@ -19,17 +19,17 @@ export class ChannelsComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
public clientsStore: ClientsStore,
|
||||
public thingsStore: ThingsStore,
|
||||
public channelsStore: ChannelsStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.channelsStore.getChannels();
|
||||
this.clientsStore.getClients();
|
||||
this.thingsStore.getThings();
|
||||
}
|
||||
|
||||
addChannel() {
|
||||
const dialogRef = this.dialog.open(AddChannelDialogComponent);
|
||||
const dialogRef = this.dialog.open(ChannelDialogComponent);
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((channel: Channel) => {
|
||||
this.channelsStore.addChannel(channel);
|
||||
@@ -37,7 +37,7 @@ export class ChannelsComponent implements OnInit {
|
||||
}
|
||||
|
||||
editChannel(channel: Channel) {
|
||||
const dialogRef = this.dialog.open(AddChannelDialogComponent, {
|
||||
const dialogRef = this.dialog.open(ChannelDialogComponent, {
|
||||
data: channel
|
||||
});
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
|
||||
import { Client } from '../../../core/store/models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-client-dialog',
|
||||
templateUrl: './add-client-dialog.component.html',
|
||||
styleUrls: ['./add-client-dialog.component.scss']
|
||||
})
|
||||
export class AddClientDialogComponent implements OnInit {
|
||||
addClientForm: FormGroup;
|
||||
@Output() submit: EventEmitter<Client> = new EventEmitter<Client>();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialogRef: MatDialogRef<AddClientDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: Client
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.addClientForm = this.fb.group(
|
||||
{
|
||||
id: [''],
|
||||
type: ['', [Validators.required]],
|
||||
name: ['', [Validators.required, Validators.minLength(5)]],
|
||||
payload: ['']
|
||||
}
|
||||
);
|
||||
|
||||
if (this.data) {
|
||||
this.addClientForm.patchValue(this.data);
|
||||
this.addClientForm.get('payload').patchValue(JSON.stringify(this.data.payload));
|
||||
}
|
||||
}
|
||||
|
||||
onAddClient() {
|
||||
const client = this.addClientForm.value;
|
||||
this.submit.emit(client);
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { toJS } from 'mobx';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Client } from '../../core/store/models';
|
||||
import { ConfirmationDialogComponent } from '../shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { AddClientDialogComponent } from './add-client-dialog/add-client-dialog.component';
|
||||
import { ClientsStore } from '../../core/store/clients.store';
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-clients',
|
||||
templateUrl: './clients.component.html',
|
||||
styleUrls: ['./clients.component.scss']
|
||||
})
|
||||
export class ClientsComponent implements OnInit {
|
||||
clients: Observable<Client[]>;
|
||||
displayedColumns = ['id', 'name', 'type', 'payload', 'actions'];
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
public clientsStore: ClientsStore,
|
||||
public channelsStore: ChannelsStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.clientsStore.getClients();
|
||||
this.channelsStore.getChannels();
|
||||
}
|
||||
|
||||
addClient() {
|
||||
const dialogRef = this.dialog.open(AddClientDialogComponent);
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((client: Client) => {
|
||||
this.clientsStore.addClient(client);
|
||||
});
|
||||
}
|
||||
|
||||
editClient(client: Client) {
|
||||
const dialogRef = this.dialog.open(AddClientDialogComponent, {
|
||||
data: client
|
||||
});
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((editedClient: Client) => {
|
||||
this.clientsStore.editClient(toJS(editedClient));
|
||||
});
|
||||
}
|
||||
|
||||
deleteClient(client: Client) {
|
||||
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
|
||||
data: {
|
||||
question: 'Are you sure you want to delete the client?'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.clientsStore.deleteClient(toJS(client));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -1,9 +1,9 @@
|
||||
<form [formGroup]="addClientForm" (ngSubmit)="onAddClient()">
|
||||
<h1 mat-dialog-title>Add client</h1>
|
||||
<form [formGroup]="addThingForm" (ngSubmit)="onAddThing()">
|
||||
<h1 mat-dialog-title>Add thing</h1>
|
||||
<div mat-dialog-content fxLayout="column">
|
||||
<input type="hidden" formControlName="id">
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="Client type" formControlName="type">
|
||||
<mat-form-field *ngIf="!editMode">
|
||||
<mat-select placeholder="Thing type" formControlName="type">
|
||||
<mat-option value="app">
|
||||
App
|
||||
</mat-option>
|
||||
@@ -11,14 +11,14 @@
|
||||
Device
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="addClientForm.get('type').errors?.required">
|
||||
<mat-error *ngIf="addThingForm.get('type').errors?.required">
|
||||
Type is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<input matInput tabindex="1" placeholder="Name" formControlName="name" />
|
||||
<mat-error *ngIf="addClientForm.get('name').errors?.required">
|
||||
<mat-error *ngIf="addThingForm.get('name').errors?.required">
|
||||
Name is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
@@ -29,6 +29,6 @@
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button mat-button mat-raised-button color="primary" type="submit" [disabled]="addClientForm.invalid">Ok</button>
|
||||
<button mat-button mat-raised-button color="primary" type="submit" [disabled]="addThingForm.invalid">Ok</button>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
+8
-8
@@ -9,16 +9,16 @@ import { MaterialModule } from '../../../core/material/material.module';
|
||||
import { AuthenticationService } from '../../../core/services/auth/authentication.service';
|
||||
import { TokenStorage } from '../../../core/services/auth/token-storage.service';
|
||||
import { ChannelsService } from '../../../core/services/channels/channels.service';
|
||||
import { ClientsService } from '../../../core/services/clients/clients.service';
|
||||
import { AddClientDialogComponent } from './add-client-dialog.component';
|
||||
import { ThingsService } from '../../../core/services/things/things.service';
|
||||
import { ThingDialogComponent } from './thing-dialog.component';
|
||||
|
||||
describe('AddClientDialogComponent', () => {
|
||||
let component: AddClientDialogComponent;
|
||||
let fixture: ComponentFixture<AddClientDialogComponent>;
|
||||
describe('ThingDialogComponent', () => {
|
||||
let component: ThingDialogComponent;
|
||||
let fixture: ComponentFixture<ThingDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddClientDialogComponent ],
|
||||
declarations: [ ThingDialogComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
@@ -31,7 +31,7 @@ describe('AddClientDialogComponent', () => {
|
||||
providers: [
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ClientsService,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
@@ -41,7 +41,7 @@ describe('AddClientDialogComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AddClientDialogComponent);
|
||||
fixture = TestBed.createComponent(ThingDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
|
||||
import { Thing } from '../../../core/store/models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-thing-dialog',
|
||||
templateUrl: './thing-dialog.component.html',
|
||||
styleUrls: ['./thing-dialog.component.scss']
|
||||
})
|
||||
export class ThingDialogComponent implements OnInit {
|
||||
addThingForm: FormGroup;
|
||||
@Output() submit: EventEmitter<Thing> = new EventEmitter<Thing>();
|
||||
editMode: boolean;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialogRef: MatDialogRef<ThingDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: Thing
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.addThingForm = this.fb.group(
|
||||
{
|
||||
id: null,
|
||||
type: ['', [Validators.required]],
|
||||
name: ['', [Validators.required, Validators.minLength(5)]],
|
||||
payload: ['']
|
||||
}
|
||||
);
|
||||
|
||||
if (this.data) {
|
||||
this.editMode = true;
|
||||
this.addThingForm.patchValue(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
onAddThing() {
|
||||
const thing = this.addThingForm.value;
|
||||
this.submit.emit(thing);
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -1,9 +1,9 @@
|
||||
<div class="clientsContainer" fxLayout="row" fxLayoutWrap>
|
||||
<ng-container [ngSwitch]="clientsStore.clients?.length > 0">
|
||||
<div class="thingsContainer" fxLayout="row" fxLayoutWrap>
|
||||
<ng-container [ngSwitch]="thingsStore.things?.length > 0">
|
||||
<ng-container *ngSwitchCase="true">
|
||||
<!-- List -->
|
||||
<div class="client-list-container">
|
||||
<mat-table #table [dataSource]="clientsStore.clients.toJS()" matSort class="mat-cell">
|
||||
<div class="things-list-container">
|
||||
<mat-table #table [dataSource]="thingsStore.things.toJS()" matSort class="mat-cell">
|
||||
<!-- Columns -->
|
||||
<ng-container matColumnDef="id">
|
||||
<mat-header-cell fxFlex="32%" *matHeaderCellDef mat-sort-header>Id</mat-header-cell>
|
||||
@@ -25,10 +25,10 @@
|
||||
<ng-container matColumnDef="actions">
|
||||
<mat-header-cell fxFlex="8%" *matHeaderCellDef mat-sort-header></mat-header-cell>
|
||||
<mat-cell fxFlex="8%" *matCellDef="let row" >
|
||||
<button mat-icon-button color="accent" (click)="editClient(row)">
|
||||
<button mat-icon-button color="accent" (click)="editThing(row)">
|
||||
<mat-icon aria-label="Delete">edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="accent" (click)="deleteClient(row)">
|
||||
<button mat-icon-button color="accent" (click)="deleteThing(row)">
|
||||
<mat-icon aria-label="Delete">delete</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
@@ -46,6 +46,6 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<button mat-mini-fab class="addButton" (click)="addClient()">
|
||||
<button mat-mini-fab class="addButton" (click)="addThing()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
+1
-1
@@ -5,6 +5,6 @@
|
||||
}
|
||||
|
||||
/* List container */
|
||||
.client-list-container {
|
||||
.things-list-container {
|
||||
width: 100%;
|
||||
}
|
||||
+11
-11
@@ -9,19 +9,19 @@ import { MaterialModule } from '../../core/material/material.module';
|
||||
import { AuthenticationService } from '../../core/services/auth/authentication.service';
|
||||
import { TokenStorage } from '../../core/services/auth/token-storage.service';
|
||||
import { ChannelsService } from '../../core/services/channels/channels.service';
|
||||
import { ClientsService } from '../../core/services/clients/clients.service';
|
||||
import { ClientsComponent } from './clients.component';
|
||||
import { ThingsService } from '../../core/services/things/things.service';
|
||||
import { ThingsComponent } from './things.component';
|
||||
import { UiStore } from '../../core/store/ui.store';
|
||||
import { ClientsStore } from '../../core/store/clients.store';
|
||||
import { ThingsStore } from '../../core/store/things.store';
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
|
||||
describe('ClientsComponent', () => {
|
||||
let component: ClientsComponent;
|
||||
let fixture: ComponentFixture<ClientsComponent>;
|
||||
describe('ThingsComponent', () => {
|
||||
let component: ThingsComponent;
|
||||
let fixture: ComponentFixture<ThingsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ClientsComponent ],
|
||||
declarations: [ ThingsComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
@@ -34,15 +34,15 @@ describe('ClientsComponent', () => {
|
||||
providers: [
|
||||
UiStore,
|
||||
{
|
||||
provide: ClientsStore,
|
||||
provide: ThingsStore,
|
||||
useClass: class {
|
||||
getClients = jasmine.createSpy('getClients');
|
||||
getThings = jasmine.createSpy('getThings');
|
||||
}
|
||||
},
|
||||
ChannelsStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ClientsService,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
@@ -52,7 +52,7 @@ describe('ClientsComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ClientsComponent);
|
||||
fixture = TestBed.createComponent(ThingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { toJS } from 'mobx';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Thing } from '../../core/store/models';
|
||||
import { ConfirmationDialogComponent } from '../shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { ThingDialogComponent } from './thing-dialog/thing-dialog.component';
|
||||
import { ThingsStore } from '../../core/store/things.store';
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-things',
|
||||
templateUrl: './things.component.html',
|
||||
styleUrls: ['./things.component.scss']
|
||||
})
|
||||
export class ThingsComponent implements OnInit {
|
||||
things: Observable<Thing[]>;
|
||||
displayedColumns = ['id', 'name', 'type', 'payload', 'actions'];
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
public thingsStore: ThingsStore,
|
||||
public channelsStore: ChannelsStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.thingsStore.getThings();
|
||||
this.channelsStore.getChannels();
|
||||
}
|
||||
|
||||
addThing() {
|
||||
const dialogRef = this.dialog.open(ThingDialogComponent);
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((thing: Thing) => {
|
||||
this.thingsStore.addThing(thing);
|
||||
});
|
||||
}
|
||||
|
||||
editThing(thing: Thing) {
|
||||
const dialogRef = this.dialog.open(ThingDialogComponent, {
|
||||
data: thing
|
||||
});
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((editedThing: Thing) => {
|
||||
this.thingsStore.editThing(toJS(editedThing));
|
||||
});
|
||||
}
|
||||
|
||||
deleteThing(thing: Thing) {
|
||||
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
|
||||
data: {
|
||||
question: 'Are you sure you want to delete the thing?'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.thingsStore.deleteThing(toJS(thing));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { Observable } from 'rxjs/Observable';
|
||||
import { forkJoin } from 'rxjs/observable/forkJoin';
|
||||
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { Channel, Client } from '../../store/models';
|
||||
import { Channel, Thing } from '../../store/models';
|
||||
|
||||
interface ChannelsPayload {
|
||||
channels: Channel[];
|
||||
@@ -36,10 +36,14 @@ export class ChannelsService {
|
||||
name: channel.name
|
||||
};
|
||||
|
||||
if (!channel.connected.length) {
|
||||
console.log('send add ch');
|
||||
return this.http.post(environment.channelsUrl, payload);
|
||||
}
|
||||
return this.http.post(environment.channelsUrl, payload, { observe: 'response' })
|
||||
.switchMap((res) => {
|
||||
const id = this.getChannelIdFrom(res);
|
||||
return forkJoin(this.createClientConnectRequests(id, channel.connected));
|
||||
return forkJoin(this.createThingsConnectRequests(id, channel.connected));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,44 +64,44 @@ export class ChannelsService {
|
||||
const editChannel = this.http.put(environment.channelsUrl + '/' + channel.id, payload);
|
||||
|
||||
return editChannel.switchMap(() => {
|
||||
const clientsToAdd = this.getClientsToAdd(channelFormData, channel);
|
||||
if (clientsToAdd.length) {
|
||||
return forkJoin(this.createClientConnectRequests(channel.id, clientsToAdd));
|
||||
const thingsToAdd = this.getThingsToAdd(channelFormData, channel);
|
||||
if (thingsToAdd.length) {
|
||||
return forkJoin(this.createThingsConnectRequests(channel.id, thingsToAdd));
|
||||
} else {
|
||||
return Observable.of([]);
|
||||
}
|
||||
}).switchMap(() => {
|
||||
const clientsToDelete = this.getClientsToDelete(channelFormData, channel);
|
||||
console.log(clientsToDelete);
|
||||
if (clientsToDelete.length) {
|
||||
return forkJoin(this.createClientDisconnectRequests(channel.id, clientsToDelete));
|
||||
const thingsToDelete = this.getThingsToDelete(channelFormData, channel);
|
||||
console.log(thingsToDelete);
|
||||
if (thingsToDelete.length) {
|
||||
return forkJoin(this.createThingsDisconnectRequests(channel.id, thingsToDelete));
|
||||
} else {
|
||||
return Observable.of([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getClientsToDelete(channelFormData: Channel, channel: Channel) {
|
||||
return channel.connected.filter(client => {
|
||||
return channelFormData.connected.find(cl => cl.id === client.id) === undefined;
|
||||
getThingsToDelete(channelFormData: Channel, channel: Channel) {
|
||||
return channel.connected.filter(thing => {
|
||||
return channelFormData.connected.find(th => th.id === thing.id) === undefined;
|
||||
});
|
||||
}
|
||||
|
||||
getClientsToAdd(channelFormData: Channel, channel: Channel) {
|
||||
return channelFormData.connected.filter(client => {
|
||||
return channel.connected.find(cl => cl.id === client.id) === undefined;
|
||||
getThingsToAdd(channelFormData: Channel, channel: Channel) {
|
||||
return channelFormData.connected.filter(thing => {
|
||||
return channel.connected.find(th => th.id === thing.id) === undefined;
|
||||
});
|
||||
}
|
||||
|
||||
createClientConnectRequests(channelId: string , connected: Client[]) {
|
||||
createThingsConnectRequests(channelId: string , connected: Thing[]) {
|
||||
return connected.map((connection) => {
|
||||
return this.http.put(environment.channelsUrl + '/' + channelId + '/clients/' + connection.id, {});
|
||||
return this.http.put(environment.channelsUrl + '/' + channelId + '/things/' + connection.id, {});
|
||||
});
|
||||
}
|
||||
|
||||
createClientDisconnectRequests(channelId: string , connected: Client[]) {
|
||||
createThingsDisconnectRequests(channelId: string , connected: Thing[]) {
|
||||
return connected.map((connection) => {
|
||||
return this.http.delete(environment.channelsUrl + '/' + channelId + '/clients/' + connection.id, {});
|
||||
return this.http.delete(environment.channelsUrl + '/' + channelId + '/things/' + connection.id, {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { Client } from '../../store/models';
|
||||
|
||||
@Injectable()
|
||||
export class ClientsService {
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
getClients() {
|
||||
return this.http.get(environment.clientsUrl);
|
||||
}
|
||||
|
||||
addClient(client: Client) {
|
||||
return this.http.post(environment.clientsUrl, client);
|
||||
}
|
||||
|
||||
deleteClient(client: Client) {
|
||||
return this.http.delete(environment.clientsUrl + '/' + client.id);
|
||||
}
|
||||
|
||||
editClient(client: Client) {
|
||||
return this.http.put(environment.clientsUrl + '/' + client.id, client);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Client } from '../store/models';
|
||||
import { Thing } from '../store/models';
|
||||
|
||||
const MOCK_CHANNELS = {
|
||||
channels: [
|
||||
@@ -20,7 +20,7 @@ export class MockChannelsService {
|
||||
return Observable.of(MOCK_CHANNELS).delay(1000);
|
||||
}
|
||||
|
||||
addChannel(client: Client) {
|
||||
addChannel(client: Thing) {
|
||||
MOCK_CHANNELS.channels.push(client);
|
||||
return Observable.of(1).delay(1000);
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Client } from '../store/models';
|
||||
|
||||
let MOCK_CLIENTS = [
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class MockClientsService {
|
||||
getClients() {
|
||||
return Observable.of(MOCK_CLIENTS).delay(1000);
|
||||
}
|
||||
|
||||
addClient(client: Client) {
|
||||
MOCK_CLIENTS.push(client);
|
||||
return Observable.of(1).delay(1000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Thing } from '../store/models';
|
||||
|
||||
const MOCK_THINGS = [
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class MockThingsService {
|
||||
getThings() {
|
||||
return Observable.of(MOCK_THINGS).delay(1000);
|
||||
}
|
||||
|
||||
addThing(thing: Thing) {
|
||||
MOCK_THINGS.push(thing);
|
||||
return Observable.of(1).delay(1000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { Thing } from '../../store/models';
|
||||
|
||||
@Injectable()
|
||||
export class ThingsService {
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
getThings() {
|
||||
return this.http.get(environment.thingsUrl);
|
||||
}
|
||||
|
||||
addThing(thing: Thing) {
|
||||
return this.http.post(environment.thingsUrl, thing);
|
||||
}
|
||||
|
||||
deleteThing(thing: Thing) {
|
||||
return this.http.delete(environment.thingsUrl + '/' + thing.id);
|
||||
}
|
||||
|
||||
editThing(thing: Thing) {
|
||||
return this.http.put(environment.thingsUrl + '/' + thing.id, thing);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { Observable } from 'rxjs/Observable';
|
||||
import { AuthenticationService } from '../services/auth/authentication.service';
|
||||
import { TokenStorage } from '../services/auth/token-storage.service';
|
||||
import { ChannelsService } from '../services/channels/channels.service';
|
||||
import { ClientsService } from '../services/clients/clients.service';
|
||||
import { ThingsService } from '../services/things/things.service';
|
||||
import { AuthStore } from './auth.store';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('AuthStore', () => {
|
||||
UiStore,
|
||||
TokenStorage,
|
||||
AuthenticationService,
|
||||
ClientsService,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
]
|
||||
});
|
||||
@@ -59,14 +59,14 @@ describe('AuthStore', () => {
|
||||
}));
|
||||
|
||||
|
||||
it('should navigate to /clients when successfully authenticated', inject([AuthStore, AuthenticationService, Router],
|
||||
it('should navigate to /things when successfully authenticated', inject([AuthStore, AuthenticationService, Router],
|
||||
(authStore: AuthStore, authService: AuthenticationService, router: Router) => {
|
||||
const spy = spyOn(authService, 'login').and.returnValue(Observable.of(true));
|
||||
const routerSpy = spyOn(router, 'navigate').and.stub();
|
||||
|
||||
authStore.login(user);
|
||||
|
||||
expect(routerSpy).toHaveBeenCalledWith(['/clients']);
|
||||
expect(routerSpy).toHaveBeenCalledWith(['/things']);
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false when successfully authenticated',
|
||||
|
||||
@@ -31,7 +31,7 @@ export class AuthStore {
|
||||
.subscribe(() => {
|
||||
this.uiState.loading = false;
|
||||
this.isAuthenticated = true;
|
||||
this.router.navigate(['/clients']);
|
||||
this.router.navigate(['/things']);
|
||||
},
|
||||
(error) => {
|
||||
this.uiState.loading = false;
|
||||
|
||||
@@ -29,8 +29,10 @@ export class ChannelsStore {
|
||||
@action
|
||||
addChannel(channel: Channel) {
|
||||
this.uiState.loading = true;
|
||||
console.log('add ch');
|
||||
this.channelsService.addChannel(channel)
|
||||
.subscribe(() => {
|
||||
.subscribe(resp => {
|
||||
console.log(resp);
|
||||
this.uiState.loading = false;
|
||||
this.getChannels();
|
||||
}, () => {
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { toJS } from 'mobx';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { ClientsService } from '../services/clients/clients.service';
|
||||
import { ClientsStore } from './clients.store';
|
||||
import { Client } from './models';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
describe('ClientsStore', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
providers: [
|
||||
ClientsStore,
|
||||
UiStore,
|
||||
ClientsService,
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ClientsStore], (clientsStore: ClientsStore) => {
|
||||
expect(clientsStore).toBeTruthy();
|
||||
}));
|
||||
|
||||
describe('getClients', () => {
|
||||
it('should set the loading flag to true before service call', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const getClients = spyOn(clientsService, 'getClients').and.returnValue({ subscribe: () => { } });
|
||||
|
||||
clientsStore.getClients();
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful get', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const getClients = spyOn(clientsService, 'getClients').and.returnValue(Observable.of(true));
|
||||
|
||||
clientsStore.getClients();
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should set the clients property to the returned clients from the service', inject([ClientsStore, ClientsService],
|
||||
(clientsStore: ClientsStore, clientsService: ClientsService) => {
|
||||
const serviceReturnValue = { clients: [] };
|
||||
const getChannels = spyOn(clientsService, 'getClients').and.returnValue(Observable.of(serviceReturnValue));
|
||||
|
||||
clientsStore.getClients();
|
||||
|
||||
expect(toJS(clientsStore.clients)).toEqual(serviceReturnValue.clients);
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed get', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const getClients = spyOn(clientsService, 'getClients').and.returnValue(Observable.throw(''));
|
||||
|
||||
clientsStore.getClients();
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('addClient', () => {
|
||||
it('should set the loading flag to true before service call', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const addClient = spyOn(clientsService, 'addClient').and.returnValue({ subscribe: () => { } });
|
||||
const newClient: Client = {
|
||||
name: 'new client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
clientsStore.addClient(newClient);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const addClient = spyOn(clientsService, 'addClient').and.returnValue(Observable.of(true));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
const newClient: Client = {
|
||||
name: 'new client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
clientsStore.addClient(newClient);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the ClientsStore.getClients after successful add', inject([ClientsStore, ClientsService],
|
||||
(clientsStore: ClientsStore, clientsService: ClientsService) => {
|
||||
const addClient = spyOn(clientsService, 'addClient').and.returnValue(Observable.of(true));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
|
||||
const newClient: Client = {
|
||||
name: 'new client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
clientsStore.addClient(newClient);
|
||||
|
||||
expect(storeGetClientsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed add', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const addClient = spyOn(clientsService, 'addClient').and.returnValue(Observable.throw(''));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
|
||||
const newClient: Client = {
|
||||
name: 'new client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
clientsStore.addClient(newClient);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('editClient', () => {
|
||||
it('should set the loading flag to true before service call', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const editClient = spyOn(clientsService, 'editClient').and.returnValue({ subscribe: () => { } });
|
||||
const editedClient: Client = {
|
||||
name: 'edited client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
clientsStore.editClient(editedClient);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const editClient = spyOn(clientsService, 'editClient').and.returnValue(Observable.of(true));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
const editedClient: Client = {
|
||||
name: 'edited client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
clientsStore.editClient(editedClient);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the ClientsStore.getChannels after successful edit', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const editClient = spyOn(clientsService, 'editClient').and.returnValue(Observable.of(true));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
|
||||
const editedClient: Client = {
|
||||
name: 'edited client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
|
||||
clientsStore.editClient(editedClient);
|
||||
|
||||
expect(storeGetClientsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed edit', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const editClient = spyOn(clientsService, 'editClient').and.returnValue(Observable.throw(''));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
|
||||
const editedClient: Client = {
|
||||
name: 'edited client',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
clientsStore.editClient(editedClient);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('deleteClient', () => {
|
||||
it('should set the loading flag to true before service call', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const deleteClient = spyOn(clientsService, 'deleteClient').and.returnValue({ subscribe: () => { } });
|
||||
const clientToBeDeleted: Client = {
|
||||
name: 'clientToBeDeleted',
|
||||
type: 'app',
|
||||
payload: ''
|
||||
};
|
||||
|
||||
clientsStore.deleteClient(clientToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const deleteClient = spyOn(clientsService, 'deleteClient').and.returnValue(Observable.of(true));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
const clientToBeDeleted: Client = {
|
||||
name: 'clientToBeDeleted',
|
||||
type: 'app',
|
||||
payload: ''
|
||||
};
|
||||
|
||||
|
||||
clientsStore.deleteClient(clientToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the clientsStore.getChannels after successful add', inject([ClientsStore, ClientsService],
|
||||
(clientsStore: ClientsStore, clientsService: ClientsService) => {
|
||||
const deleteClient = spyOn(clientsService, 'deleteClient').and.returnValue(Observable.of(true));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
|
||||
const clientToBeDeleted: Client = {
|
||||
name: 'clientToBeDeleted',
|
||||
type: 'app',
|
||||
payload: ''
|
||||
};
|
||||
|
||||
clientsStore.deleteClient(clientToBeDeleted);
|
||||
|
||||
expect(storeGetClientsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed delete', inject([ClientsStore, UiStore, ClientsService],
|
||||
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
|
||||
const deleteClient = spyOn(clientsService, 'deleteClient').and.returnValue(Observable.throw(''));
|
||||
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
|
||||
const clientToBeDeleted: Client = {
|
||||
name: 'clientToBeDeleted',
|
||||
type: 'app',
|
||||
payload: ''
|
||||
};
|
||||
|
||||
|
||||
clientsStore.deleteClient(clientToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
export interface Channel {
|
||||
id?: '';
|
||||
name: string;
|
||||
connected: Client[];
|
||||
connected: Thing[];
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
export interface Thing {
|
||||
id?: '';
|
||||
type: string;
|
||||
name: string;
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { toJS } from 'mobx';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { ThingsService } from '../services/things/things.service';
|
||||
import { ThingsStore } from './things.store';
|
||||
import { Thing } from './models';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
describe('ThingsStore', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
providers: [
|
||||
ThingsStore,
|
||||
UiStore,
|
||||
ThingsService,
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ThingsStore], (thingsStore: ThingsStore) => {
|
||||
expect(thingsStore).toBeTruthy();
|
||||
}));
|
||||
|
||||
describe('getThings', () => {
|
||||
it('should set the loading flag to true before service call', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const getThings = spyOn(thingsService, 'getThings').and.returnValue({ subscribe: () => { } });
|
||||
|
||||
thingsStore.getThings();
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful get', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const getThings = spyOn(thingsService, 'getThings').and.returnValue(Observable.of(true));
|
||||
|
||||
thingsStore.getThings();
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should set the things property to the returned things from the service', inject([ThingsStore, ThingsService],
|
||||
(thingsStore: ThingsStore, thingsService: ThingsService) => {
|
||||
const serviceReturnValue = { things: [] };
|
||||
const getChannels = spyOn(thingsService, 'getThings').and.returnValue(Observable.of(serviceReturnValue));
|
||||
|
||||
thingsStore.getThings();
|
||||
|
||||
expect(toJS(thingsStore.things)).toEqual(serviceReturnValue.things);
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed get', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const getThings = spyOn(thingsService, 'getThings').and.returnValue(Observable.throw(''));
|
||||
|
||||
thingsStore.getThings();
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('addThing', () => {
|
||||
it('should set the loading flag to true before service call', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const addThing = spyOn(thingsService, 'addThing').and.returnValue({ subscribe: () => { } });
|
||||
const newThing: Thing = {
|
||||
name: 'new thing',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
thingsStore.addThing(newThing);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const addThing = spyOn(thingsService, 'addThing').and.returnValue(Observable.of(true));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
const newThing: Thing = {
|
||||
name: 'new thing',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
thingsStore.addThing(newThing);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the thingsStore.getThings after successful add', inject([ThingsStore, ThingsService],
|
||||
(thingsStore: ThingsStore, thingsService: ThingsService) => {
|
||||
const addThing = spyOn(thingsService, 'addThing').and.returnValue(Observable.of(true));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
const newThing: Thing = {
|
||||
name: 'new thing',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
thingsStore.addThing(newThing);
|
||||
|
||||
expect(storeGetThingsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed add', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const addThing = spyOn(thingsService, 'addThing').and.returnValue(Observable.throw(''));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
|
||||
const newThing: Thing = {
|
||||
name: 'new thing',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
thingsStore.addThing(newThing);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('editThing', () => {
|
||||
it('should set the loading flag to true before service call', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const editThing = spyOn(thingsService, 'editThing').and.returnValue({ subscribe: () => { } });
|
||||
const editedThing: Thing = {
|
||||
name: 'edited thing',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
thingsStore.editThing(editedThing);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const editThing = spyOn(thingsService, 'editThing').and.returnValue(Observable.of(true));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
const editedThing: Thing = {
|
||||
name: 'edited thing',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
thingsStore.editThing(editedThing);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the thingsStore.getChannels after successful edit', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const editThing = spyOn(thingsService, 'editThing').and.returnValue(Observable.of(true));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
|
||||
const editedThing: Thing = {
|
||||
name: 'edited thing',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
thingsStore.editThing(editedThing);
|
||||
|
||||
expect(storeGetThingsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed edit', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const editThing = spyOn(thingsService, 'editThing').and.returnValue(Observable.throw(''));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
const editedThing: Thing = {
|
||||
name: 'edited thing',
|
||||
type: 'app',
|
||||
payload: '',
|
||||
};
|
||||
|
||||
thingsStore.editThing(editedThing);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('deleteThing', () => {
|
||||
it('should set the loading flag to true before service call', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const deleteThing = spyOn(thingsService, 'deleteThing').and.returnValue({ subscribe: () => { } });
|
||||
const thingToBeDeleted: Thing = {
|
||||
name: 'thingToBeDeleted',
|
||||
type: 'app',
|
||||
payload: ''
|
||||
};
|
||||
|
||||
thingsStore.deleteThing(thingToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after successful service call', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const deleteThing = spyOn(thingsService, 'deleteThing').and.returnValue(Observable.of(true));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
const thingToBeDeleted: Thing = {
|
||||
name: 'thingToBeDeleted',
|
||||
type: 'app',
|
||||
payload: ''
|
||||
};
|
||||
|
||||
|
||||
thingsStore.deleteThing(thingToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should call the thingsStore.getChannels after successful add', inject([ThingsStore, ThingsService],
|
||||
(thingsStore: ThingsStore, thingsService: ThingsService) => {
|
||||
const deleteThing = spyOn(thingsService, 'deleteThing').and.returnValue(Observable.of(true));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
|
||||
const thingToBeDeleted: Thing = {
|
||||
name: 'thingToBeDeleted',
|
||||
type: 'app',
|
||||
payload: ''
|
||||
};
|
||||
|
||||
thingsStore.deleteThing(thingToBeDeleted);
|
||||
|
||||
expect(storeGetThingsSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set the loading flag to false after failed delete', inject([ThingsStore, UiStore, ThingsService],
|
||||
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
|
||||
const deleteThing = spyOn(thingsService, 'deleteThing').and.returnValue(Observable.throw(''));
|
||||
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
|
||||
const thingToBeDeleted: Thing = {
|
||||
name: 'thingToBeDeleted',
|
||||
type: 'app',
|
||||
payload: ''
|
||||
};
|
||||
|
||||
thingsStore.deleteThing(thingToBeDeleted);
|
||||
|
||||
expect(uiStore.loading).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
});
|
||||
+17
-17
@@ -1,62 +1,62 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { action, observable } from 'mobx';
|
||||
|
||||
import { ClientsService } from '../services/clients/clients.service';
|
||||
import { Client } from './models';
|
||||
import { ThingsService } from '../services/things/things.service';
|
||||
import { Thing } from './models';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
@Injectable()
|
||||
export class ClientsStore {
|
||||
@observable clients: Client[] = [];
|
||||
export class ThingsStore {
|
||||
@observable things: Thing[] = [];
|
||||
|
||||
constructor(
|
||||
private uiState: UiStore,
|
||||
private clientsService: ClientsService,
|
||||
private thingsService: ThingsService,
|
||||
) { }
|
||||
|
||||
@action
|
||||
getClients() {
|
||||
getThings() {
|
||||
this.uiState.loading = true;
|
||||
this.clientsService.getClients()
|
||||
this.thingsService.getThings()
|
||||
.subscribe((payload: any) => {
|
||||
this.uiState.loading = false;
|
||||
this.clients = payload.clients;
|
||||
this.things = payload.things;
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
addClient(client: Client) {
|
||||
addThing(thing: Thing) {
|
||||
this.uiState.loading = true;
|
||||
this.clientsService.addClient(client)
|
||||
this.thingsService.addThing(thing)
|
||||
.subscribe(() => {
|
||||
this.uiState.loading = false;
|
||||
this.getClients();
|
||||
this.getThings();
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
editClient(client: Client) {
|
||||
editThing(thing: Thing) {
|
||||
this.uiState.loading = true;
|
||||
this.clientsService.editClient(client)
|
||||
this.thingsService.editThing(thing)
|
||||
.subscribe(() => {
|
||||
this.uiState.loading = false;
|
||||
this.getClients();
|
||||
this.getThings();
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
deleteClient(client: Client) {
|
||||
deleteThing(thing: Thing) {
|
||||
this.uiState.loading = true;
|
||||
this.clientsService.deleteClient(client)
|
||||
this.thingsService.deleteThing(thing)
|
||||
.subscribe(() => {
|
||||
this.uiState.loading = false;
|
||||
this.getClients();
|
||||
this.getThings();
|
||||
}, () => {
|
||||
this.uiState.loading = false;
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AuthenticationService } from '../services/auth/authentication.service';
|
||||
import { TokenStorage } from '../services/auth/token-storage.service';
|
||||
import { ChannelsService } from '../services/channels/channels.service';
|
||||
import { ClientsService } from '../services/clients/clients.service';
|
||||
import { ThingsService } from '../services/things/things.service';
|
||||
import { UiStore } from './ui.store';
|
||||
|
||||
describe('State', () => {
|
||||
@@ -21,7 +21,7 @@ describe('State', () => {
|
||||
UiStore,
|
||||
TokenStorage,
|
||||
AuthenticationService,
|
||||
ClientsService,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
]
|
||||
});
|
||||
|
||||
@@ -2,6 +2,6 @@ export const environment = {
|
||||
production: true,
|
||||
signupUrl: '/users',
|
||||
loginUrl: '/tokens',
|
||||
clientsUrl: '/clients',
|
||||
thingsUrl: '/things',
|
||||
channelsUrl: '/channels'
|
||||
};
|
||||
|
||||
@@ -7,6 +7,6 @@ export const environment = {
|
||||
production: false,
|
||||
signupUrl: '/users',
|
||||
loginUrl: '/tokens',
|
||||
clientsUrl: '/clients',
|
||||
thingsUrl: '/things',
|
||||
channelsUrl: '/channels'
|
||||
};
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
###
|
||||
# This docker-compose file contains optional InfluxDB, InfluxDB-writer and Grafana services
|
||||
# for Mainflux platform. Since these are optional, this file is dependent of docker-compose file
|
||||
# from <project_root>/docker. In order to run these optional service, execute command:
|
||||
# docker-compose -f docker/docker-compose.yml -f docker/addons/influxdb/docker-compose.yml up
|
||||
# from project root.
|
||||
###
|
||||
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
|
||||
influxdb:
|
||||
image: influxdb:1.5.2-alpine
|
||||
container_name: mainflux-influxdb
|
||||
restart: on-failure
|
||||
environment:
|
||||
INFLUXDB_DB: mainflux
|
||||
INFLUXDB_ADMIN_USER: mainflux
|
||||
INFLUXDB_ADMIN_PASSWORD: mainflux
|
||||
|
||||
influxdb-writer:
|
||||
image: mainflux/influxdb:latest
|
||||
container_name: mainflux-influxdb-writer
|
||||
depends_on:
|
||||
- influxdb
|
||||
- nats
|
||||
expose:
|
||||
- 8900
|
||||
restart: on-failure
|
||||
environment:
|
||||
MF_NATS_URL: nats://nats:4222
|
||||
MF_INFLUX_WRITER_PORT: 8900
|
||||
MF_INFLUX_WRITER_DB_NAME: mainflux
|
||||
MF_INFLUX_WRITER_DB_HOST: influxdb
|
||||
MF_INFLUX_WRITER_DB_PORT: 8086
|
||||
MF_INFLUX_WRITER_DB_USER: mainflux
|
||||
MF_INFLUX_WRITER_DB_PASS: mainflux
|
||||
ports:
|
||||
- 8900:8900
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:5.1.3
|
||||
container_name: mainflux-grafana
|
||||
depends_on:
|
||||
- influxdb
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 3001:3000
|
||||
@@ -0,0 +1,41 @@
|
||||
###
|
||||
# This docker-compose file contains optional MongoDB and MongoDB-writer services
|
||||
# for Mainflux platform. Since these are optional, this file is dependent of docker-compose file
|
||||
# from <project_root>/docker. In order to run these optional service, execute command:
|
||||
# docker-compose -f docker/docker-compose.yml -f docker/addons/mongodb/docker-compose.yml up
|
||||
# from project root. MongoDB default port (27017) is exposed, so you can use various tools for database
|
||||
# inspection and data visualization.
|
||||
###
|
||||
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
|
||||
mongodb:
|
||||
image: mongo:3.6-jessie
|
||||
container_name: mainflux-mongodb
|
||||
expose:
|
||||
- 27017
|
||||
restart: on-failure
|
||||
environment:
|
||||
MONGO_INITDB_DATABASE: mainflux
|
||||
ports:
|
||||
- 27017:27017
|
||||
|
||||
mongo-writer:
|
||||
image: mainflux/mongodb:latest
|
||||
container_name: mainflux-mongodb-writer
|
||||
depends_on:
|
||||
- mongodb
|
||||
- nats
|
||||
expose:
|
||||
- 8901
|
||||
restart: on-failure
|
||||
environment:
|
||||
MF_NATS_URL: nats://nats:4222
|
||||
MF_MONGO_WRITER_PORT: 8901
|
||||
MF_MONGO_WRITER_DB_NAME: mainflux
|
||||
MF_MONGO_WRITER_DB_HOST: mongodb
|
||||
MF_MONGO_WRITER_DB_PORT: 27017
|
||||
ports:
|
||||
- 8901:8901
|
||||
+51
-44
@@ -14,14 +14,14 @@ services:
|
||||
container_name: mainflux-nginx
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- $PWD/nginx.conf:/etc/nginx/nginx.conf
|
||||
- $PWD/ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
|
||||
- $PWD/ssl/certs/mainflux-server.key:/etc/ssl/private/mainflux-server.key
|
||||
- $PWD/ssl/dhparam.pem:/etc/ssl/certs/dhparam.pem
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
|
||||
- ./ssl/certs/mainflux-server.key:/etc/ssl/private/mainflux-server.key
|
||||
- ./ssl/dhparam.pem:/etc/ssl/certs/dhparam.pem
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8883:8883"
|
||||
- 80:80
|
||||
- 443:443
|
||||
- 8883:8883
|
||||
|
||||
nats:
|
||||
image: nats:1.0.2
|
||||
@@ -58,35 +58,35 @@ services:
|
||||
ports:
|
||||
- 8180:8180
|
||||
|
||||
clients-db:
|
||||
things-db:
|
||||
image: postgres:10.2-alpine
|
||||
container_name: mainflux-clients-db
|
||||
container_name: mainflux-things-db
|
||||
restart: on-failure
|
||||
environment:
|
||||
POSTGRES_USER: mainflux
|
||||
POSTGRES_PASSWORD: mainflux
|
||||
POSTGRES_DB: clients
|
||||
POSTGRES_DB: things
|
||||
|
||||
clients:
|
||||
image: mainflux/clients:latest
|
||||
container_name: mainflux-clients
|
||||
things:
|
||||
image: mainflux/things:latest
|
||||
container_name: mainflux-things
|
||||
depends_on:
|
||||
- clients-db
|
||||
- things-db
|
||||
- users
|
||||
expose:
|
||||
- 8182
|
||||
- 8183
|
||||
restart: on-failure
|
||||
environment:
|
||||
MF_CLIENTS_DB_HOST: clients-db
|
||||
MF_CLIENTS_DB_PORT: 5432
|
||||
MF_CLIENTS_DB_USER: mainflux
|
||||
MF_CLIENTS_DB_PASS: mainflux
|
||||
MF_CLIENTS_DB: clients
|
||||
MF_CLIENTS_HTTP_PORT: 8182
|
||||
MF_CLIENTS_GRPC_PORT: 8183
|
||||
MF_THINGS_DB_HOST: things-db
|
||||
MF_THINGS_DB_PORT: 5432
|
||||
MF_THINGS_DB_USER: mainflux
|
||||
MF_THINGS_DB_PASS: mainflux
|
||||
MF_THINGS_DB: things
|
||||
MF_THINGS_HTTP_PORT: 8182
|
||||
MF_THINGS_GRPC_PORT: 8183
|
||||
MF_USERS_URL: users:8181
|
||||
MF_CLIENTS_SECRET: secret
|
||||
MF_THINGS_SECRET: secret
|
||||
ports:
|
||||
- 8182:8182
|
||||
|
||||
@@ -95,52 +95,59 @@ services:
|
||||
container_name: mainflux-normalizer
|
||||
restart: on-failure
|
||||
expose:
|
||||
- 8181
|
||||
- 8184
|
||||
environment:
|
||||
MF_NATS_URL: nats://nats:4222
|
||||
MF_NORMALIZER_PORT: 8181
|
||||
MF_NORMALIZER_PORT: 8184
|
||||
|
||||
coap:
|
||||
image: mainflux/coap:latest
|
||||
container_name: mainflux-coap
|
||||
depends_on:
|
||||
- clients
|
||||
expose:
|
||||
- 5683
|
||||
dashflux:
|
||||
image: mainflux/dashflux:latest
|
||||
container_name: mainflux-dashflux
|
||||
restart: on-failure
|
||||
environment:
|
||||
MF_COAP_ADAPTER_PORT: 5683
|
||||
MF_NATS_URL: nats://nats:4222
|
||||
MF_CLIENTS_URL: clients:8183
|
||||
ports:
|
||||
- 5683:5683/udp
|
||||
- 3000:3000
|
||||
|
||||
ws:
|
||||
ws-adapter:
|
||||
image: mainflux/ws:latest
|
||||
container_name: mainflux-ws
|
||||
depends_on:
|
||||
- clients
|
||||
- things
|
||||
expose:
|
||||
- 8184
|
||||
- 8186
|
||||
restart: on-failure
|
||||
environment:
|
||||
MF_WS_ADAPTER_PORT: 8184
|
||||
MF_WS_ADAPTER_PORT: 8186
|
||||
MF_NATS_URL: nats://nats:4222
|
||||
MF_CLIENTS_URL: clients:8183
|
||||
MF_THINGS_URL: things:8183
|
||||
ports:
|
||||
- 8184:8184
|
||||
- 8186:8186
|
||||
|
||||
http-adapter:
|
||||
image: mainflux/http:latest
|
||||
container_name: mainflux-http
|
||||
depends_on:
|
||||
- clients
|
||||
- things
|
||||
restart: on-failure
|
||||
expose:
|
||||
- 8185
|
||||
environment:
|
||||
MF_HTTP_ADAPTER_PORT: 8185
|
||||
MF_NATS_URL: nats://nats:4222
|
||||
MF_CLIENTS_URL: clients:8183
|
||||
MF_THINGS_URL: things:8183
|
||||
ports:
|
||||
- 8185:8185
|
||||
|
||||
mqtt-adapter:
|
||||
image: mainflux/mqtt:latest
|
||||
container_name: mainflux-mqtt
|
||||
depends_on:
|
||||
- things
|
||||
restart: on-failure
|
||||
environment:
|
||||
MF_MQTT_ADAPTER_PORT: 1883
|
||||
MF_MQTT_WS_PORT: 8880
|
||||
MF_NATS_URL: nats://nats:4222
|
||||
MF_THINGS_URL: things:8183
|
||||
ports:
|
||||
- 1883:1883
|
||||
- 8880:8880
|
||||
|
||||
+148
-43
@@ -116,59 +116,164 @@ http {
|
||||
add_header Access-Control-Allow-Methods '*';
|
||||
add_header Access-Control-Allow-Headers "*";
|
||||
|
||||
|
||||
server_name localhost;
|
||||
|
||||
# Proxy pass to users service
|
||||
location ~ ^/(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://users:8180;
|
||||
|
||||
# Proxy pass to users service
|
||||
location ~ ^/(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://users:8180;
|
||||
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Proxy pass to clients service
|
||||
location ~ ^/(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://clients:8182;
|
||||
# Proxy pass to things service
|
||||
location ~ ^/(things|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;
|
||||
add_header Access-Control-Expose-Headers Location;
|
||||
proxy_pass http://things:8182;
|
||||
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
# 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/;
|
||||
location /version {
|
||||
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://things:8182;
|
||||
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
# 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;
|
||||
}
|
||||
}
|
||||
|
||||
# Proxy pass to mainflux-ws-adapter
|
||||
location /ws/ {
|
||||
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_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_connect_timeout 7d;
|
||||
proxy_send_timeout 7d;
|
||||
proxy_read_timeout 7d;
|
||||
proxy_pass http://ws-adapter:8186/;
|
||||
|
||||
# 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-mqtt-adapter
|
||||
location /mqtt {
|
||||
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_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_connect_timeout 7d;
|
||||
proxy_send_timeout 7d;
|
||||
proxy_read_timeout 7d;
|
||||
proxy_pass http://mqtt-adapter:8880/;
|
||||
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
|
||||
location / {
|
||||
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://dashflux:3000/;
|
||||
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# MQTT
|
||||
stream {
|
||||
# MQTT
|
||||
server {
|
||||
listen 8883 ssl;
|
||||
listen [::]:8883 ssl;
|
||||
|
||||
# Certificates
|
||||
ssl_certificate /etc/ssl/certs/mainflux-server.crt;
|
||||
ssl_certificate_key /etc/ssl/private/mainflux-server.key;
|
||||
ssl_dhparam /etc/ssl/certs/dhparam.pem;
|
||||
|
||||
|
||||
# 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;
|
||||
resolver 8.8.8.8 8.8.4.4 valid=300s;
|
||||
resolver_timeout 5s;
|
||||
|
||||
proxy_pass mqtt-adapter:1883;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-15
@@ -5,7 +5,7 @@ Mainflux IoT platform is comprised of the following services:
|
||||
| Service | Description |
|
||||
|:--------------------------------------------------------------------------|:------------------------------------------------------------------------|
|
||||
| [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 |
|
||||
| [things](https://github.com/mainflux/mainflux/tree/master/things) | Manages platform's things, channels and access policies |
|
||||
| [normalizer](https://github.com/mainflux/mainflux/tree/master/normalizer) | Normalizes SenML messages and generates the "processed" messages stream |
|
||||
| [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 |
|
||||
@@ -14,26 +14,22 @@ Mainflux IoT platform is comprised of the following services:
|
||||
|
||||
## Domain model
|
||||
|
||||
The platform is built around 3 main entities: **users**, **clients** and **channels**.
|
||||
The platform is built around 3 main entities: **users**, **things** and **channels**.
|
||||
|
||||
`User` represents the real (human) user of the system. It is represented via its
|
||||
e-mail and password, which he uses as platform access credentials in order to obtain
|
||||
an access token. Once logged into the system, user can manage his resources in
|
||||
CRUD fashion (i.e. channels and clients), and define access control policies
|
||||
between them.
|
||||
an access token. Once logged into the system, user can manage his resources (i.e.
|
||||
things and channels) in CRUD fashion, and define access control policies by
|
||||
connecting them.
|
||||
|
||||
`Device` is used to represent any device that connects to Mainflux. It is a
|
||||
generic model that describes any client device of the system.
|
||||
|
||||
`Application` is very similar to the `Device` and is represented by the same
|
||||
`Client` structure (just with different `type` info). Application represents
|
||||
an end-user application that communicates with devices through Mainflux, and
|
||||
can be running somewhere in the cloud, locally on the PC or on the mobile phone.
|
||||
Usually it acquires data from sensor measurement and displays it on various
|
||||
dashboards.
|
||||
`Thing` represents devices and applications connected to Mainflux. There are
|
||||
two types of "things" supported at the moment: `device` and `app`. While device
|
||||
is used to represent any physical device connected to the platform, app represents
|
||||
any 3rd party service that uses the platform for message exchange with other
|
||||
"things".
|
||||
|
||||
`Channel` represents a communication channel. It serves as message topic that
|
||||
can be consumed by all of the clients connected to it.
|
||||
can be consumed by all of the things connected to it.
|
||||
|
||||
## Messaging
|
||||
|
||||
|
||||
+43
-47
@@ -48,56 +48,56 @@ an authorization key.
|
||||
|
||||
### Provisioning devices
|
||||
|
||||
Devices are provisioned by executing request `POST /clients`, with a
|
||||
Devices are provisioned by executing request `POST /things`, with a
|
||||
`"type":"device"` specified in JSON payload. Note that you will also need
|
||||
`user_auth_token` in order to provision clients (both devices and application)
|
||||
`user_auth_token` in order to provision things (both devices and application)
|
||||
that belong to this particular user.
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json" -H "Authorization: <user_auth_token>" https://localhost/clients -d '{"type":"device", "name":"weio"}'
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json" -H "Authorization: <user_auth_token>" https://localhost/things -d '{"type":"device", "name":"weio"}'
|
||||
```
|
||||
|
||||
Response will contain `Location` header whose value represents path to newly
|
||||
created client:
|
||||
created thing:
|
||||
|
||||
```
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json
|
||||
Location: /clients/81380742-7116-4f6f-9800-14fe464f6773
|
||||
Location: /things/81380742-7116-4f6f-9800-14fe464f6773
|
||||
Date: Tue, 10 Apr 2018 10:02:59 GMT
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
### Provisioning applications
|
||||
|
||||
Applications are provisioned by executing HTTP request `POST /clients`, with
|
||||
Applications are provisioned by executing HTTP request `POST /things`, with
|
||||
`"type":"app"` specified in JSON payload.
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json" -H "Authorization: <user_auth_token>" https://localhost/clients -d '{"type":"app", "name":"myapp"}'
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json" -H "Authorization: <user_auth_token>" https://localhost/things -d '{"type":"app", "name":"myapp"}'
|
||||
```
|
||||
|
||||
Response will contain `Location` header whose value represents path to newly
|
||||
created client (same as for devices):
|
||||
created thing (same as for devices):
|
||||
|
||||
```
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json
|
||||
Location: /clients/cb63f852-2d48-44f0-a0cf-e450496c6c92
|
||||
Location: /things/cb63f852-2d48-44f0-a0cf-e450496c6c92
|
||||
Date: Tue, 10 Apr 2018 10:33:17 GMT
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
### Retrieving provisioned clients
|
||||
### Retrieving provisioned things
|
||||
|
||||
In order to retrieve data of provisioned clients that is written in database, you
|
||||
In order to retrieve data of provisioned things that is written in database, you
|
||||
can send following request:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/clients
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/things
|
||||
```
|
||||
|
||||
Notice that you will receive only those clients that were provisioned by
|
||||
Notice that you will receive only those things that were provisioned by
|
||||
`user_auth_token` owner.
|
||||
|
||||
```
|
||||
@@ -107,7 +107,7 @@ Date: Tue, 10 Apr 2018 10:50:12 GMT
|
||||
Content-Length: 1105
|
||||
|
||||
{
|
||||
"clients": [
|
||||
"things": [
|
||||
{
|
||||
"id": "81380742-7116-4f6f-9800-14fe464f6773",
|
||||
"type": "device",
|
||||
@@ -125,22 +125,22 @@ Content-Length: 1105
|
||||
```
|
||||
|
||||
You can specify `offset` and `limit` parameters in order to fetch specific
|
||||
group of clients. In that case, your request should look like:
|
||||
group of things. In that case, your request should look like:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/clients?offset=0&limit=5
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/things?offset=0&limit=5
|
||||
```
|
||||
|
||||
If you don't provide them, default values will be used instead: 0 for `offset`,
|
||||
and 10 for `limit`. Note that `limit` cannot be set to values greater than 100. Providing
|
||||
invalid values will be considered malformed request.
|
||||
|
||||
### Removing clients
|
||||
### Removing things
|
||||
|
||||
In order to remove you own client you can send following request:
|
||||
In order to remove you own thing you can send following request:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/clients/<client_id>
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/things/<thing_id>
|
||||
```
|
||||
|
||||
### Provisioning channels
|
||||
@@ -211,28 +211,28 @@ curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE
|
||||
|
||||
## Access control
|
||||
|
||||
Channel can be observed as a communication group of clients. Only clients that
|
||||
are connected to the channel can send and receive messages from other clients
|
||||
in this channel. Clients that are not connected to this channel are not allowed
|
||||
Channel can be observed as a communication group of things. Only things that
|
||||
are connected to the channel can send and receive messages from other things
|
||||
in this channel. things that are not connected to this channel are not allowed
|
||||
to communicate over it.
|
||||
|
||||
Only user, who is the owner of a channel and of the clients, can connect the
|
||||
clients to the channel (which is equivalent of giving permissions to these clients
|
||||
Only user, who is the owner of a channel and of the things, can connect the
|
||||
things to the channel (which is equivalent of giving permissions to these things
|
||||
to communicate over given communication group).
|
||||
|
||||
To connect client to the channel you should send following request:
|
||||
To connect thing to the channel you should send following request:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X PUT -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>/clients/<client_id>
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X PUT -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>/things/<thing_id>
|
||||
```
|
||||
|
||||
You can observe which clients are connected to specific channel:
|
||||
You can observe which things are connected to specific channel:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>
|
||||
```
|
||||
|
||||
You should receive response with the lists of connected clients in `connected` field
|
||||
You should receive response with the lists of connected things in `connected` field
|
||||
similar to this one:
|
||||
|
||||
```
|
||||
@@ -253,21 +253,21 @@ similar to this one:
|
||||
If you want to disconnect your device from the channel, send following request:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>/clients/<client_id>
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>/things/<thing_id>
|
||||
```
|
||||
|
||||
## Sending messages
|
||||
|
||||
Once a channel is provisioned and client is connected to it, it can start to
|
||||
Once a channel is provisioned and thing is connected to it, it can start to
|
||||
publish messages on the channel. The following sections will provide an example
|
||||
of message publishing for each of the supported protocols.
|
||||
|
||||
### HTTP
|
||||
|
||||
To publish message over channel, client should send following request:
|
||||
To publish message over channel, thing should send following request:
|
||||
|
||||
```
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/senml+json" -H "Authorization: <client_token>" https://localhost/http/channels/<channel_id>/messages -d '[{"bn":"some-base-name:","bt":1.276020076001e+09, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]'
|
||||
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/senml+json" -H "Authorization: <thing_token>" https://localhost/http/channels/<channel_id>/messages -d '[{"bn":"some-base-name:","bt":1.276020076001e+09, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]'
|
||||
```
|
||||
|
||||
Note that you should always send array of messages in senML format.
|
||||
@@ -276,32 +276,28 @@ Note that you should always send array of messages in senML format.
|
||||
|
||||
To publish and receive messages over channel using web socket, you should first
|
||||
send handshake request to `/channels/<channel_id>/messages` path. Don't forget
|
||||
to send `Authorization` header with client authorization token.
|
||||
to send `Authorization` header with thing authorization token.
|
||||
|
||||
If you are not able to send custom headers in your handshake request, send it as
|
||||
query parameter `authorization`. Then your path should look like this
|
||||
`/channels/<channel_id>/messages?authorization=<client_auth_key>`.
|
||||
`/channels/<channel_id>/messages?authorization=<thing_auth_key>`.
|
||||
|
||||
### CoAP
|
||||
### MQTT
|
||||
|
||||
These are two useful tools for sending CoAP messages:
|
||||
To send and receive messages over MQTT you could use [Mosquitto tools](https://mosquitto.org),
|
||||
or [Paho](https://www.eclipse.org/paho/) if you want to use MQTT over WebSocket.
|
||||
|
||||
- [Copper](https://github.com/mkovatsc/Copper)
|
||||
- [coap-cli](https://github.com/mcollina/coap-cli)
|
||||
|
||||
To publish message over channel, client should send following confirmable message:
|
||||
To publish message over channel, thing should call following command:
|
||||
|
||||
```
|
||||
coap post coap://localhost/channels/<channel_id>/messages?key=<client_token>
|
||||
mosquitto_pub -u <thing_id> -P <thing_key> -t channels/<channel_id>/messages -h localhost -m [{"bn":"some-base-name:","bt":1.276020076001e+09, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]
|
||||
```
|
||||
|
||||
Client token must be present in `Uri-Query` option. Otherwise, message will be considered unauthorized.
|
||||
|
||||
To subscribe to channel, client should send following confirmable message:
|
||||
To subscribe to channel, thing should call following command:
|
||||
|
||||
```
|
||||
coap get coap://localhost/channels/<channel_id>/messages?key=<client_token> -o
|
||||
mosquitto_sub -u <thing_id> -P <thing_key> -t channels/<channel_id>/messages -h localhost
|
||||
```
|
||||
|
||||
where `-o` in indicates that value of `Observe` option is 0.
|
||||
To unsubscribe from channel, client should send same message with value of `Observe` option set to 1, ignore confirmable message received from service (by default, server will attempt to send confirmable message multiple times, as described in [RFC 7252](https://tools.ietf.org/html/rfc7252#page-28) or send `Reset` message with appropriate token.
|
||||
If you are using TLS to secure MQTT connection, add `--cafile docker/ssl/certs/ca.crt`
|
||||
to every command.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
@@ -1 +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>
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:59.0) Gecko/20100101 Firefox/59.0" version="8.6.4" editor="www.draw.io" type="device"><diagram id="82652b06-2c2b-a339-9c87-b7b8398097a5" name="Page-1">7Vpbc6M2FP41flwPd+PHtTeb7UyacZt0uvuUkUEGdQVihBzb++srgYQB4YQm+JImyWQCB13gO993dHQZ2fNke01BFv9OQohHlhFuR/aXkWWZjm3wf8KyKy1TyywNEUWhLLQ33KFfUBplvWiNQpg3CjJCMENZ0xiQNIUBa9gApWTTLLYiuNlrBiKoGe4CgHXr3yhkcWn1XWNv/wZRFKueTUM+WYLgZ0TJOpX9jSx7VfyUjxOg2pLl8xiEZFMz2Vcje04JYeVVsp1DLLBVsJX1vh54Wr03hSnrU2FSVngEeC0//fb6t9vv8uXYTgHyCClDHJ8bsIR4QXLEEEn5oyVhjCQjewbyrHTECm0hb3wWswTzW5NfqsqfMYpEJUYybs0ZJT/hnGBCuS0lKe9qRtYMo5RblV8NUTIGmXiPZBsJuo0DxCjajjNKtruHHFLePi+lf7pEQ3QPtzWThOIakgQyuuNF5NOKf5K2lueW95s9CSbeWBrjGgOmsiKQxIuqtvfg8wuJ/wHneZoz/uJfl2vOwMIJ/Is3MWLwLgOBsG84NE3YUVKyHCUR7zrAiGPD+NU1BPTBtPwt/xtnacSLrhDGyhOcsiGA/irQfMSfeIEPl6th0LanTbRNJa8a2qYSSh1sbwCwbVMD+/rPxVzDGoY8MMhbQllMIpICfLW3zgq1C8oXXK3hD7eIfRdmTpjy7ocsxDGju9ojcftD1soZ99JnEcO4IcAgz1GgzF8RVo3/AxnbycAJ1owI7VSvd0OEwopyB/2UkzUNoOK9jLGARlAWs6alTQDwpDcpxIChx2bkfI1rLN01C0oYCQgv5WH+frMl5VeRuAIhyNg7EoljnFAkrn0cPfiThiIqfSwgRfwlRTgvA38/LcA01Atx49HkYutymZxLLa6jqWXkzjIlGPeL5kLOPNb0DIU5+gWWRQGBe0ZQyop3cmeiBS4zOXQHHJditG2P6QkKw8L9he5mVRpUk4VMhLpAVkTTdFGlePLtGmlSl14+GWPDNOQI3Rts2dxCfHetCFmtcu7gtjeqXvuFM1tz0BwjKADWxIV5/MlhLeUJMFmHz0exVmiCZujCSVdomnoTG3jDhKZ2sjTpGZn8ASJTR+LKs1IWUXj3x42Ga4XmjieXYcHfZwBdlkHsZnkAYY6vHzpdCPvW0vYGQtj1WhD7OsTTYyVI1keCtI/4Uz3iq3T9DAnSVHPNfYzS6N0kQV06ON5MwdUHWE0EXRnIAaLWBVDRvE7ykvO9eDnRaamKPUvLGnZuB3TK9tIxVE2p7Vb+2nZJ+TmyVn2lot2Q32rIbDVUgqA19IIR29b19fZSqoq2r06peEZlGX4D/E/m68gxaIJl6+smFy3QvhPr0wjUbAnUm7xQoGZLoJ51PIH6mse/3d8v7t6ELhVbB5nquPYrpzrbzgrD6FLP0bu9dEHi7J3UfYjzkNsdffnuFB5Xkwk1sfiPXOjK8Hsv6nyQ4SAZ9MWPdzBP79rJONY83elY/7t8uak4W5eb4sqFyM21mk612yrpKzd38kxDA8pNn6vefr7XR9xSDWpr22q6vb3dusJkE8ScLOMQUZ6fPISAgQPSbMlv5YrfzuWB4mcg+bluA2DH1Ldtqx36OlPsAfTnTXS5DbAk1mt/pBKoqQs0BHlc9KCtmcmt9vqCmXGGXRTH10OAd7Y1NUefUrxD5bimPnAdTTlOxzJLr5GrQy/6MPTk4CVu22I6LJjDi8y9iN61u64C9YWMdW8xtVSffnr6NJjSFZf7xb/LokA73XGcF1JgYrQaaseKASngH2XsbWxHNRLhjnG2M5aceLxtELIX+dyOtQ81BJ7hDEPHMURCE4D5p1PNxf+TTS1NJ6c82ePpa+bzdS6OcmonrAKS5uvkLEesVn4Ag05HLH3XcZ8Mta84YjXtmD4M5Ah+uz/cW0a8/Qlq++pf</diagram></mxfile>
|
||||
+7
-7
@@ -4,9 +4,9 @@ Testing environment to be determined.
|
||||
|
||||
### Message publishing
|
||||
|
||||
In this scenario, large number of requests are sent to HTTP adapter service
|
||||
every second. This test checks how much time HTTP adapter took to response to
|
||||
each request.
|
||||
In this scenario, large number of requests are sent to HTTP adapter service
|
||||
every second. This test checks how much time HTTP adapter needs to respond
|
||||
to each request.
|
||||
|
||||
#### Results
|
||||
|
||||
@@ -14,10 +14,10 @@ TBD
|
||||
|
||||
### Create and get client
|
||||
|
||||
In this scenario, large number of requests are sent to manager service to create
|
||||
client, and than to retrieve its data. This test checks how much time manager
|
||||
service took to response to each request.
|
||||
In this scenario, large number of requests are sent to things service to create
|
||||
things, and than to retrieve their data. This test checks how much time things
|
||||
service needs to respond to each request.
|
||||
|
||||
#### Results
|
||||
|
||||
TBD
|
||||
TBD
|
||||
|
||||
+4
-4
@@ -10,9 +10,9 @@ default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------------------|---------------------|-----------------------|
|
||||
| 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 |
|
||||
| MF_NATS_URL | NATS instance URL | nats://localhost:4222 |
|
||||
| MF_THINGS_URL | Things service URL | localhost:8181 |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -28,7 +28,7 @@ services:
|
||||
ports:
|
||||
- [host machine port]:8180
|
||||
environment:
|
||||
MF_CLIENTS_URL: [Clients service URL]
|
||||
MF_THINGS_URL: [Things 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_CLIENTS_URL=[Clients service URL] MF_NATS_URL=[NATS instance URL] MF_HTTP_ADAPTER_PORT=[Service HTTP port] $GOBIN/mainflux-http
|
||||
MF_THINGS_URL=[Things service URL] MF_NATS_URL=[NATS instance URL] MF_HTTP_ADAPTER_PORT=[Service HTTP port] $GOBIN/mainflux-http
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
+39
-23
@@ -15,27 +15,16 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
token = "auth_token"
|
||||
invalidToken = "invalid_token"
|
||||
msg = `[{"n":"current","t":-1,"v":1.6}]`
|
||||
)
|
||||
|
||||
func newService() mainflux.MessagePublisher {
|
||||
pub := mocks.NewPublisher()
|
||||
return adapter.New(pub)
|
||||
}
|
||||
|
||||
func newHTTPServer(pub mainflux.MessagePublisher, cc mainflux.ClientsServiceClient) *httptest.Server {
|
||||
func newHTTPServer(pub mainflux.MessagePublisher, cc mainflux.ThingsServiceClient) *httptest.Server {
|
||||
mux := api.MakeHandler(pub, cc)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func newClientsClient() mainflux.ClientsServiceClient {
|
||||
return mocks.NewClientsClient(map[string]string{token: id})
|
||||
}
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
@@ -60,32 +49,59 @@ func (tr testRequest) make() (*http.Response, error) {
|
||||
}
|
||||
|
||||
func TestPublish(t *testing.T) {
|
||||
clientsClient := newClientsClient()
|
||||
id := uint64(1)
|
||||
contentType := "application/senml+json"
|
||||
token := "auth_token"
|
||||
invalidToken := "invalid_token"
|
||||
msg := `[{"n":"current","t":-1,"v":1.6}]`
|
||||
|
||||
thingsClient := mocks.NewThingsClient(map[string]uint64{token: id})
|
||||
pub := newService()
|
||||
ts := newHTTPServer(pub, clientsClient)
|
||||
ts := newHTTPServer(pub, thingsClient)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
cases := map[string]struct {
|
||||
chanID string
|
||||
chanID uint64
|
||||
msg string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
"publish message": {id, msg, "application/senml+json", token, http.StatusAccepted},
|
||||
"publish message with no authorization token": {id, msg, "application/senml+json", "", http.StatusForbidden},
|
||||
"publish message with invalid authorization token": {id, msg, "application/senml+json", invalidToken, http.StatusForbidden},
|
||||
"publish message with no content type": {id, msg, "", token, http.StatusAccepted},
|
||||
"publish message with invalid channel id": {"1", msg, "application/senml+json", token, http.StatusNotFound},
|
||||
"publish message": {
|
||||
chanID: id,
|
||||
msg: msg,
|
||||
contentType: contentType,
|
||||
auth: token,
|
||||
status: http.StatusAccepted,
|
||||
},
|
||||
"publish message without authorization token": {
|
||||
chanID: id,
|
||||
msg: msg,
|
||||
contentType: contentType,
|
||||
auth: "",
|
||||
status: http.StatusForbidden,
|
||||
},
|
||||
"publish message with invalid authorization token": {
|
||||
chanID: id,
|
||||
msg: msg,
|
||||
contentType: contentType,
|
||||
auth: invalidToken,
|
||||
status: http.StatusForbidden,
|
||||
},
|
||||
"publish message without content type": {
|
||||
chanID: id,
|
||||
msg: msg,
|
||||
contentType: "",
|
||||
auth: token,
|
||||
status: http.StatusAccepted,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
client: ts.Client(),
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/channels/%s/messages", ts.URL, tc.chanID),
|
||||
url: fmt.Sprintf("%s/channels/%d/messages", ts.URL, tc.chanID),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.msg),
|
||||
|
||||
+18
-14
@@ -8,26 +8,25 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const protocol string = "http"
|
||||
const protocol = "http"
|
||||
|
||||
var (
|
||||
errMalformedData = errors.New("malformed SenML data")
|
||||
errNotFound = errors.New("non-existent entity")
|
||||
auth mainflux.ClientsServiceClient
|
||||
auth mainflux.ThingsServiceClient
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc mainflux.MessagePublisher, cc mainflux.ClientsServiceClient) http.Handler {
|
||||
func MakeHandler(svc mainflux.MessagePublisher, cc mainflux.ThingsServiceClient) http.Handler {
|
||||
auth = cc
|
||||
|
||||
opts := []kithttp.ServerOption{
|
||||
@@ -60,36 +59,41 @@ func decodeRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channel, err := things.FromString(bone.GetValue(r, "id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg := mainflux.RawMessage{
|
||||
Publisher: publisher,
|
||||
Protocol: protocol,
|
||||
ContentType: r.Header.Get("Content-Type"),
|
||||
Channel: bone.GetValue(r, "id"),
|
||||
Channel: channel,
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func authorize(r *http.Request) (string, error) {
|
||||
func authorize(r *http.Request) (uint64, error) {
|
||||
apiKey := r.Header.Get("Authorization")
|
||||
|
||||
if apiKey == "" {
|
||||
return "", clients.ErrUnauthorizedAccess
|
||||
return 0, things.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
// extract ID from /channels/:id/messages
|
||||
c := bone.GetValue(r, "id")
|
||||
if !govalidator.IsUUID(c) {
|
||||
return "", errNotFound
|
||||
chanID, err := things.FromString(bone.GetValue(r, "id"))
|
||||
if err != nil {
|
||||
return 0, things.ErrNotFound
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := auth.CanAccess(ctx, &mainflux.AccessReq{Token: apiKey, ChanID: c})
|
||||
id, err := auth.CanAccess(ctx, &mainflux.AccessReq{Token: apiKey, ChanID: chanID})
|
||||
if err != nil {
|
||||
return "", err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return id.GetValue(), nil
|
||||
@@ -116,7 +120,7 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errNotFound:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case clients.ErrUnauthorizedAccess:
|
||||
case things.ErrUnauthorizedAccess:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
default:
|
||||
if e, ok := status.FromError(err); ok {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
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{Value: id}, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.ThingsServiceClient = (*thingsClient)(nil)
|
||||
|
||||
type thingsClient struct {
|
||||
things map[string]uint64
|
||||
}
|
||||
|
||||
// NewThingsClient returns mock implementation of things service client.
|
||||
func NewThingsClient(data map[string]uint64) mainflux.ThingsServiceClient {
|
||||
return &thingsClient{data}
|
||||
}
|
||||
|
||||
func (tc thingsClient) CanAccess(ctx context.Context, req *mainflux.AccessReq, opts ...grpc.CallOption) (*mainflux.ThingID, error) {
|
||||
key := req.GetToken()
|
||||
if key == "" {
|
||||
return nil, things.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
id, ok := tc.things[key]
|
||||
if !ok {
|
||||
return nil, things.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return &mainflux.ThingID{Value: id}, nil
|
||||
}
|
||||
|
||||
func (tc thingsClient) Identify(ctx context.Context, req *mainflux.Token, opts ...grpc.CallOption) (*mainflux.ThingID, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -26,6 +26,6 @@ func (pub *natsPublisher) Publish(msg mainflux.RawMessage) error {
|
||||
return err
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("channel.%s", msg.Channel)
|
||||
subject := fmt.Sprintf("channel.%d", msg.Channel)
|
||||
return pub.nc.Publish(subject, data)
|
||||
}
|
||||
|
||||
+10
-5
@@ -2,23 +2,28 @@ syntax = "proto3";
|
||||
|
||||
package mainflux;
|
||||
|
||||
service ClientsService {
|
||||
rpc CanAccess(AccessReq) returns (Identity) {}
|
||||
service ThingsService {
|
||||
rpc CanAccess(AccessReq) returns (ThingID) {}
|
||||
rpc Identify(Token) returns (ThingID) {}
|
||||
}
|
||||
|
||||
service UsersService {
|
||||
rpc Identify(Token) returns (mainflux.Identity) {}
|
||||
rpc Identify(Token) returns (UserID) {}
|
||||
}
|
||||
|
||||
message AccessReq {
|
||||
string token = 1;
|
||||
string chanID = 2;
|
||||
uint64 chanID = 2;
|
||||
}
|
||||
|
||||
message ThingID {
|
||||
uint64 value = 1;
|
||||
}
|
||||
|
||||
message Token {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message Identity {
|
||||
message UserID {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user