This implementation is similar to AMQP notifications: * Notifications are published on a single topic as a JSON feed * Topic is configurable, as is the QoS. Uses the paho.mqtt.golang library for the mqtt connection, and supports connections over tcp and websockets, with optional secure tls support. * Additionally the minio server configuration has been bumped up so mqtt configuration can be added. * Configuration migration code is added with tests. MQTT is an ISO standard M2M/IoT messaging protocol and was originally designed for applications for limited bandwidth networks. Today it's use is growing in the IoT space.
419 lines
9.2 KiB
Go
419 lines
9.2 KiB
Go
/*
|
|
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Test validates for duplicate configs.
|
|
func TestCheckDuplicateConfigs(t *testing.T) {
|
|
testCases := []struct {
|
|
qConfigs []queueConfig
|
|
expectedErrCode APIErrorCode
|
|
}{
|
|
// Error for duplicate queue configs.
|
|
{
|
|
qConfigs: []queueConfig{
|
|
{
|
|
QueueARN: "arn:minio:sqs:us-east-1:1:redis",
|
|
},
|
|
{
|
|
QueueARN: "arn:minio:sqs:us-east-1:1:redis",
|
|
},
|
|
},
|
|
expectedErrCode: ErrOverlappingConfigs,
|
|
},
|
|
// Valid queue configs.
|
|
{
|
|
qConfigs: []queueConfig{
|
|
{
|
|
QueueARN: "arn:minio:sqs:us-east-1:1:redis",
|
|
},
|
|
},
|
|
expectedErrCode: ErrNone,
|
|
},
|
|
}
|
|
|
|
// ... validate for duplicate queue configs.
|
|
for i, testCase := range testCases {
|
|
errCode := checkDuplicateQueueConfigs(testCase.qConfigs)
|
|
if errCode != testCase.expectedErrCode {
|
|
t.Errorf("Test %d: Expected %d, got %d", i+1, testCase.expectedErrCode, errCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tests for validating filter rules.
|
|
func TestCheckFilterRules(t *testing.T) {
|
|
testCases := []struct {
|
|
rules []filterRule
|
|
expectedErrCode APIErrorCode
|
|
}{
|
|
// Valid prefix and suffix values.
|
|
{
|
|
rules: []filterRule{
|
|
{
|
|
Name: "prefix",
|
|
Value: "test/test1",
|
|
},
|
|
{
|
|
Name: "suffix",
|
|
Value: ".jpg",
|
|
},
|
|
},
|
|
expectedErrCode: ErrNone,
|
|
},
|
|
// Invalid filter name.
|
|
{
|
|
rules: []filterRule{
|
|
{
|
|
Name: "unknown",
|
|
Value: "test/test1",
|
|
},
|
|
},
|
|
expectedErrCode: ErrFilterNameInvalid,
|
|
},
|
|
// Cannot have duplicate prefixes.
|
|
{
|
|
rules: []filterRule{
|
|
{
|
|
Name: "prefix",
|
|
Value: "test/test1",
|
|
},
|
|
{
|
|
Name: "prefix",
|
|
Value: "test/test1",
|
|
},
|
|
},
|
|
expectedErrCode: ErrFilterNamePrefix,
|
|
},
|
|
// Cannot have duplicate suffixes.
|
|
{
|
|
rules: []filterRule{
|
|
{
|
|
Name: "suffix",
|
|
Value: ".jpg",
|
|
},
|
|
{
|
|
Name: "suffix",
|
|
Value: ".txt",
|
|
},
|
|
},
|
|
expectedErrCode: ErrFilterNameSuffix,
|
|
},
|
|
// Filter value cannot be bigger than > 1024.
|
|
{
|
|
rules: []filterRule{
|
|
{
|
|
Name: "prefix",
|
|
Value: strings.Repeat("a", 1025),
|
|
},
|
|
},
|
|
expectedErrCode: ErrFilterValueInvalid,
|
|
},
|
|
}
|
|
|
|
for i, testCase := range testCases {
|
|
errCode := checkFilterRules(testCase.rules)
|
|
if errCode != testCase.expectedErrCode {
|
|
t.Errorf("Test %d: Expected %d, got %d", i+1, testCase.expectedErrCode, errCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tests filter name validation.
|
|
func TestIsValidFilterName(t *testing.T) {
|
|
testCases := []struct {
|
|
filterName string
|
|
status bool
|
|
}{
|
|
// Validate if 'prefix' is correct.
|
|
{
|
|
filterName: "prefix",
|
|
status: true,
|
|
},
|
|
// Validate if 'suffix' is correct.
|
|
{
|
|
filterName: "suffix",
|
|
status: true,
|
|
},
|
|
// Invalid filter name empty string should return false.
|
|
{
|
|
filterName: "",
|
|
status: false,
|
|
},
|
|
// Invalid filter name random character should return false.
|
|
{
|
|
filterName: "unknown",
|
|
status: false,
|
|
},
|
|
}
|
|
|
|
for i, testCase := range testCases {
|
|
status := isValidFilterName(testCase.filterName)
|
|
if testCase.status != status {
|
|
t.Errorf("Test %d: Expected \"%t\", got \"%t\"", i+1, testCase.status, status)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tests list of valid and invalid events.
|
|
func TestValidEvents(t *testing.T) {
|
|
testCases := []struct {
|
|
events []string
|
|
errCode APIErrorCode
|
|
}{
|
|
// Return error for unknown event element.
|
|
{
|
|
events: []string{
|
|
"s3:UnknownAPI",
|
|
},
|
|
errCode: ErrEventNotification,
|
|
},
|
|
// Return success for supported event.
|
|
{
|
|
events: []string{
|
|
"s3:ObjectCreated:Put",
|
|
},
|
|
errCode: ErrNone,
|
|
},
|
|
// Return success for supported events.
|
|
{
|
|
events: []string{
|
|
"s3:ObjectCreated:*",
|
|
"s3:ObjectRemoved:*",
|
|
},
|
|
errCode: ErrNone,
|
|
},
|
|
// Return error for empty event list.
|
|
{
|
|
events: []string{""},
|
|
errCode: ErrEventNotification,
|
|
},
|
|
}
|
|
|
|
for i, testCase := range testCases {
|
|
errCode := checkEvents(testCase.events)
|
|
if testCase.errCode != errCode {
|
|
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tests queue arn validation.
|
|
func TestQueueARN(t *testing.T) {
|
|
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
if err != nil {
|
|
t.Fatalf("unable initialize config file, %s", err)
|
|
}
|
|
defer removeAll(rootPath)
|
|
|
|
testCases := []struct {
|
|
queueARN string
|
|
errCode APIErrorCode
|
|
}{
|
|
|
|
// Valid webhook queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:1:webhook",
|
|
errCode: ErrNone,
|
|
},
|
|
// Valid redis queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:1:redis",
|
|
errCode: ErrNone,
|
|
},
|
|
// Valid elasticsearch queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:1:elasticsearch",
|
|
errCode: ErrNone,
|
|
},
|
|
// Valid amqp queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:1:amqp",
|
|
errCode: ErrNone,
|
|
},
|
|
// Invalid empty queue arn.
|
|
{
|
|
queueARN: "",
|
|
errCode: ErrARNNotification,
|
|
},
|
|
// Invalid notification service type.
|
|
{
|
|
queueARN: "arn:minio:sns:us-east-1:1:listen",
|
|
errCode: ErrARNNotification,
|
|
},
|
|
// Invalid queue name empty in queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:1:",
|
|
errCode: ErrARNNotification,
|
|
},
|
|
// Invalid queue id empty in queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1::redis",
|
|
errCode: ErrARNNotification,
|
|
},
|
|
// Invalid queue id and queue name empty in queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1::",
|
|
errCode: ErrARNNotification,
|
|
},
|
|
// Missing queue id and separator missing at the end in queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:amqp",
|
|
errCode: ErrARNNotification,
|
|
},
|
|
// Missing queue id and empty string at the end in queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:",
|
|
errCode: ErrARNNotification,
|
|
},
|
|
}
|
|
|
|
// Validate all tests for queue arn.
|
|
for i, testCase := range testCases {
|
|
errCode := checkQueueARN(testCase.queueARN)
|
|
if testCase.errCode != errCode {
|
|
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
|
|
}
|
|
}
|
|
|
|
// Test when server region is set.
|
|
rootPath, err = newTestConfig("us-east-1")
|
|
if err != nil {
|
|
t.Fatalf("unable initialize config file, %s", err)
|
|
}
|
|
defer removeAll(rootPath)
|
|
|
|
testCases = []struct {
|
|
queueARN string
|
|
errCode APIErrorCode
|
|
}{
|
|
// Incorrect region should produce error.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-west-1:1:webhook",
|
|
errCode: ErrRegionNotification,
|
|
},
|
|
// Correct region should not produce error.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:1:webhook",
|
|
errCode: ErrNone,
|
|
},
|
|
}
|
|
|
|
// Validate all tests for queue arn.
|
|
for i, testCase := range testCases {
|
|
errCode := checkQueueARN(testCase.queueARN)
|
|
if testCase.errCode != errCode {
|
|
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test unmarshal queue arn.
|
|
func TestUnmarshalSQSARN(t *testing.T) {
|
|
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
if err != nil {
|
|
t.Fatalf("unable initialize config file, %s", err)
|
|
}
|
|
defer removeAll(rootPath)
|
|
|
|
testCases := []struct {
|
|
queueARN string
|
|
Type string
|
|
}{
|
|
// Valid webhook queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:1:webhook",
|
|
Type: "webhook",
|
|
},
|
|
// Valid redis queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:1:redis",
|
|
Type: "redis",
|
|
},
|
|
// Valid elasticsearch queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:1:elasticsearch",
|
|
Type: "elasticsearch",
|
|
},
|
|
// Valid amqp queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:1:amqp",
|
|
Type: "amqp",
|
|
},
|
|
// Valid mqtt queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:1:mqtt",
|
|
Type: "mqtt",
|
|
},
|
|
// Invalid empty queue arn.
|
|
{
|
|
queueARN: "",
|
|
Type: "",
|
|
},
|
|
// Partial queue arn.
|
|
{
|
|
queueARN: "arn:minio:sqs:",
|
|
Type: "",
|
|
},
|
|
// Invalid queue service value.
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:1:*",
|
|
Type: "",
|
|
},
|
|
}
|
|
|
|
for i, testCase := range testCases {
|
|
mSqs := unmarshalSqsARN(testCase.queueARN)
|
|
if testCase.Type != mSqs.Type {
|
|
t.Errorf("Test %d: Expected \"%s\", got \"%s\"", i+1, testCase.Type, mSqs.Type)
|
|
}
|
|
}
|
|
|
|
// Test when the server region is set.
|
|
rootPath, err = newTestConfig("us-east-1")
|
|
if err != nil {
|
|
t.Fatalf("unable initialize config file, %s", err)
|
|
}
|
|
defer removeAll(rootPath)
|
|
|
|
testCases = []struct {
|
|
queueARN string
|
|
Type string
|
|
}{
|
|
// Incorrect region in ARN returns empty mSqs.Type
|
|
{
|
|
queueARN: "arn:minio:sqs:us-west-1:1:webhook",
|
|
Type: "",
|
|
},
|
|
// Correct regionin ARN returns valid mSqs.Type
|
|
{
|
|
queueARN: "arn:minio:sqs:us-east-1:1:webhook",
|
|
Type: "webhook",
|
|
},
|
|
}
|
|
|
|
for i, testCase := range testCases {
|
|
mSqs := unmarshalSqsARN(testCase.queueARN)
|
|
if testCase.Type != mSqs.Type {
|
|
t.Errorf("Test %d: Expected \"%s\", got \"%s\"", i+1, testCase.Type, mSqs.Type)
|
|
}
|
|
}
|
|
}
|