make notification as separate package (#5294)

* Remove old notification files

* Add net package

* Add event package

* Modify minio to take new notification system
This commit is contained in:
Bala FA
2018-03-16 01:33:41 +05:30
committed by kannappanr
parent abffa00b76
commit 0e4431725c
117 changed files with 7677 additions and 9296 deletions

150
pkg/net/host.go Normal file
View File

@@ -0,0 +1,150 @@
/*
* Minio Cloud Storage, (C) 2018 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 net
import (
"encoding/json"
"errors"
"net"
"regexp"
"strings"
)
var hostLabelRegexp = regexp.MustCompile("^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$")
// Host - holds network host IP/name and its port.
type Host struct {
Name string
Port Port
IsPortSet bool
}
// IsEmpty - returns whether Host is empty or not
func (host Host) IsEmpty() bool {
return host.Name == ""
}
// String - returns string representation of Host.
func (host Host) String() string {
if !host.IsPortSet {
return host.Name
}
return host.Name + ":" + host.Port.String()
}
// Equal - checks whether given host is equal or not.
func (host Host) Equal(compHost Host) bool {
return host.String() == compHost.String()
}
// MarshalJSON - converts Host into JSON data
func (host Host) MarshalJSON() ([]byte, error) {
return json.Marshal(host.String())
}
// UnmarshalJSON - parses data into Host.
func (host *Host) UnmarshalJSON(data []byte) (err error) {
var s string
if err = json.Unmarshal(data, &s); err != nil {
return err
}
// Allow empty string
if s == "" {
*host = Host{}
return nil
}
var h *Host
if h, err = ParseHost(s); err != nil {
return err
}
*host = *h
return nil
}
// ParseHost - parses string into Host
func ParseHost(s string) (*Host, error) {
isValidHost := func(host string) bool {
if host == "" {
return false
}
if ip := net.ParseIP(host); ip != nil {
return true
}
// host is not a valid IPv4 or IPv6 address
// host may be a hostname
// refer https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// why checks are done like below
if len(host) < 1 || len(host) > 253 {
return false
}
for _, label := range strings.Split(host, ".") {
if len(label) < 1 || len(label) > 63 {
return false
}
if !hostLabelRegexp.MatchString(label) {
return false
}
}
return true
}
var port Port
var isPortSet bool
host, portStr, err := net.SplitHostPort(s)
if err != nil {
if !strings.Contains(err.Error(), "missing port in address") {
return nil, err
}
host = s
portStr = ""
} else {
if port, err = ParsePort(portStr); err != nil {
return nil, err
}
isPortSet = true
}
if !isValidHost(host) {
return nil, errors.New("invalid hostname")
}
return &Host{
Name: host,
Port: port,
IsPortSet: isPortSet,
}, nil
}
// MustParseHost - parses given string to Host, else panics.
func MustParseHost(s string) *Host {
host, err := ParseHost(s)
if err != nil {
panic(err)
}
return host
}

236
pkg/net/host_test.go Normal file
View File

@@ -0,0 +1,236 @@
/*
* Minio Cloud Storage, (C) 2018 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 net
import (
"reflect"
"testing"
)
func TestHostIsEmpty(t *testing.T) {
testCases := []struct {
host Host
expectedResult bool
}{
{Host{"", 0, false}, true},
{Host{"", 0, true}, true},
{Host{"play", 9000, false}, false},
{Host{"play", 9000, true}, false},
}
for i, testCase := range testCases {
result := testCase.host.IsEmpty()
if result != testCase.expectedResult {
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestHostString(t *testing.T) {
testCases := []struct {
host Host
expectedStr string
}{
{Host{"", 0, false}, ""},
{Host{"", 0, true}, ":0"},
{Host{"play", 9000, false}, "play"},
{Host{"play", 9000, true}, "play:9000"},
}
for i, testCase := range testCases {
str := testCase.host.String()
if str != testCase.expectedStr {
t.Fatalf("test %v: string: expected: %v, got: %v", i+1, testCase.expectedStr, str)
}
}
}
func TestHostEqual(t *testing.T) {
testCases := []struct {
host Host
compHost Host
expectedResult bool
}{
{Host{"", 0, false}, Host{"", 0, true}, false},
{Host{"play", 9000, true}, Host{"play", 9000, false}, false},
{Host{"", 0, true}, Host{"", 0, true}, true},
{Host{"play", 9000, false}, Host{"play", 9000, false}, true},
{Host{"play", 9000, true}, Host{"play", 9000, true}, true},
}
for i, testCase := range testCases {
result := testCase.host.Equal(testCase.compHost)
if result != testCase.expectedResult {
t.Fatalf("test %v: string: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestHostMarshalJSON(t *testing.T) {
testCases := []struct {
host Host
expectedData []byte
expectErr bool
}{
{Host{}, []byte(`""`), false},
{Host{"play", 0, false}, []byte(`"play"`), false},
{Host{"play", 0, true}, []byte(`"play:0"`), false},
{Host{"play", 9000, true}, []byte(`"play:9000"`), false},
{Host{"play.minio.io", 0, false}, []byte(`"play.minio.io"`), false},
{Host{"play.minio.io", 9000, true}, []byte(`"play.minio.io:9000"`), false},
{Host{"147.75.201.93", 0, false}, []byte(`"147.75.201.93"`), false},
{Host{"147.75.201.93", 9000, true}, []byte(`"147.75.201.93:9000"`), false},
{Host{"play12", 0, false}, []byte(`"play12"`), false},
{Host{"12play", 0, false}, []byte(`"12play"`), false},
{Host{"play-minio-io", 0, false}, []byte(`"play-minio-io"`), false},
{Host{"play--minio.io", 0, false}, []byte(`"play--minio.io"`), false},
}
for i, testCase := range testCases {
data, err := testCase.host.MarshalJSON()
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(data, testCase.expectedData) {
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, string(testCase.expectedData), string(data))
}
}
}
}
func TestHostUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedHost *Host
expectErr bool
}{
{[]byte(`""`), &Host{}, false},
{[]byte(`"play"`), &Host{"play", 0, false}, false},
{[]byte(`"play:0"`), &Host{"play", 0, true}, false},
{[]byte(`"play:9000"`), &Host{"play", 9000, true}, false},
{[]byte(`"play.minio.io"`), &Host{"play.minio.io", 0, false}, false},
{[]byte(`"play.minio.io:9000"`), &Host{"play.minio.io", 9000, true}, false},
{[]byte(`"147.75.201.93"`), &Host{"147.75.201.93", 0, false}, false},
{[]byte(`"147.75.201.93:9000"`), &Host{"147.75.201.93", 9000, true}, false},
{[]byte(`"play12"`), &Host{"play12", 0, false}, false},
{[]byte(`"12play"`), &Host{"12play", 0, false}, false},
{[]byte(`"play-minio-io"`), &Host{"play-minio-io", 0, false}, false},
{[]byte(`"play--minio.io"`), &Host{"play--minio.io", 0, false}, false},
{[]byte(`":9000"`), nil, true},
{[]byte(`"play:"`), nil, true},
{[]byte(`"play::"`), nil, true},
{[]byte(`"play:90000"`), nil, true},
{[]byte(`"play:-10"`), nil, true},
{[]byte(`"play-"`), nil, true},
{[]byte(`"play.minio..io"`), nil, true},
{[]byte(`":"`), nil, true},
}
for i, testCase := range testCases {
var host Host
err := host.UnmarshalJSON(testCase.data)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(&host, testCase.expectedHost) {
t.Fatalf("test %v: host: expected: %#v, got: %#v", i+1, testCase.expectedHost, host)
}
}
}
}
func TestParseHost(t *testing.T) {
testCases := []struct {
s string
expectedHost *Host
expectErr bool
}{
{"play", &Host{"play", 0, false}, false},
{"play:0", &Host{"play", 0, true}, false},
{"play:9000", &Host{"play", 9000, true}, false},
{"play.minio.io", &Host{"play.minio.io", 0, false}, false},
{"play.minio.io:9000", &Host{"play.minio.io", 9000, true}, false},
{"147.75.201.93", &Host{"147.75.201.93", 0, false}, false},
{"147.75.201.93:9000", &Host{"147.75.201.93", 9000, true}, false},
{"play12", &Host{"play12", 0, false}, false},
{"12play", &Host{"12play", 0, false}, false},
{"play-minio-io", &Host{"play-minio-io", 0, false}, false},
{"play--minio.io", &Host{"play--minio.io", 0, false}, false},
{":9000", nil, true},
{"play:", nil, true},
{"play::", nil, true},
{"play:90000", nil, true},
{"play:-10", nil, true},
{"play-", nil, true},
{"play.minio..io", nil, true},
{":", nil, true},
{"", nil, true},
}
for i, testCase := range testCases {
host, err := ParseHost(testCase.s)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(host, testCase.expectedHost) {
t.Fatalf("test %v: host: expected: %#v, got: %#v", i+1, testCase.expectedHost, host)
}
}
}
}
func TestMustParseHost(t *testing.T) {
testCases := []struct {
s string
expectedHost *Host
}{
{"play", &Host{"play", 0, false}},
{"play:0", &Host{"play", 0, true}},
{"play:9000", &Host{"play", 9000, true}},
{"play.minio.io", &Host{"play.minio.io", 0, false}},
{"play.minio.io:9000", &Host{"play.minio.io", 9000, true}},
{"147.75.201.93", &Host{"147.75.201.93", 0, false}},
{"147.75.201.93:9000", &Host{"147.75.201.93", 9000, true}},
{"play12", &Host{"play12", 0, false}},
{"12play", &Host{"12play", 0, false}},
{"play-minio-io", &Host{"play-minio-io", 0, false}},
{"play--minio.io", &Host{"play--minio.io", 0, false}},
}
for i, testCase := range testCases {
host := MustParseHost(testCase.s)
if !reflect.DeepEqual(host, testCase.expectedHost) {
t.Fatalf("test %v: host: expected: %#v, got: %#v", i+1, testCase.expectedHost, host)
}
}
}

54
pkg/net/port.go Normal file
View File

@@ -0,0 +1,54 @@
/*
* Minio Cloud Storage, (C) 2018 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 net
import (
"errors"
"strconv"
)
// Port - network port
type Port uint16
// String - returns string representation of port.
func (p Port) String() string {
return strconv.Itoa(int(p))
}
// ParsePort - parses string into Port
func ParsePort(s string) (p Port, err error) {
var i int
if i, err = strconv.Atoi(s); err != nil {
return p, errors.New("invalid port number")
}
if i < 0 || i > 65535 {
return p, errors.New("port must be between 0 to 65535")
}
return Port(i), nil
}
// MustParsePort - parses string into Port, else panics
func MustParsePort(s string) Port {
p, err := ParsePort(s)
if err != nil {
panic(err)
}
return p
}

92
pkg/net/port_test.go Normal file
View File

@@ -0,0 +1,92 @@
/*
* Minio Cloud Storage, (C) 2018 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 net
import (
"testing"
)
func TestPortString(t *testing.T) {
testCases := []struct {
port Port
expectedStr string
}{
{Port(0), "0"},
{Port(9000), "9000"},
{Port(65535), "65535"},
{Port(1024), "1024"},
}
for i, testCase := range testCases {
str := testCase.port.String()
if str != testCase.expectedStr {
t.Fatalf("test %v: error: port: %v, got: %v", i+1, testCase.expectedStr, str)
}
}
}
func TestParsePort(t *testing.T) {
testCases := []struct {
s string
expectedPort Port
expectErr bool
}{
{"0", Port(0), false},
{"9000", Port(9000), false},
{"65535", Port(65535), false},
{"90000", Port(0), true},
{"-10", Port(0), true},
{"", Port(0), true},
{"http", Port(0), true},
{" 1024", Port(0), true},
}
for i, testCase := range testCases {
port, err := ParsePort(testCase.s)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if port != testCase.expectedPort {
t.Fatalf("test %v: error: port: %v, got: %v", i+1, testCase.expectedPort, port)
}
}
}
}
func TestMustParsePort(t *testing.T) {
testCases := []struct {
s string
expectedPort Port
}{
{"0", Port(0)},
{"9000", Port(9000)},
{"65535", Port(65535)},
}
for i, testCase := range testCases {
port := MustParsePort(testCase.s)
if port != testCase.expectedPort {
t.Fatalf("test %v: error: port: %v, got: %v", i+1, testCase.expectedPort, port)
}
}
}

103
pkg/net/url.go Normal file
View File

@@ -0,0 +1,103 @@
/*
* Minio Cloud Storage, (C) 2018 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 net
import (
"encoding/json"
"errors"
"net/url"
"path"
)
// URL - improved JSON friendly url.URL.
type URL url.URL
// IsEmpty - checks URL is empty or not.
func (u URL) IsEmpty() bool {
return u.String() == ""
}
// String - returns string representation of URL.
func (u URL) String() string {
// if port number 80 and 443, remove for http and https scheme respectively
if u.Host != "" {
host := MustParseHost(u.Host)
switch {
case u.Scheme == "http" && host.Port == 80:
fallthrough
case u.Scheme == "https" && host.Port == 443:
u.Host = host.Name
}
}
uu := url.URL(u)
return uu.String()
}
// MarshalJSON - converts to JSON string data.
func (u URL) MarshalJSON() ([]byte, error) {
return json.Marshal(u.String())
}
// UnmarshalJSON - parses given data into URL.
func (u *URL) UnmarshalJSON(data []byte) (err error) {
var s string
if err = json.Unmarshal(data, &s); err != nil {
return err
}
// Allow empty string
if s == "" {
*u = URL{}
return nil
}
var ru *URL
if ru, err = ParseURL(s); err != nil {
return err
}
*u = *ru
return nil
}
// ParseURL - parses string into URL.
func ParseURL(s string) (u *URL, err error) {
var uu *url.URL
if uu, err = url.Parse(s); err != nil {
return nil, err
}
if uu.Host == "" {
if uu.Scheme != "" {
return nil, errors.New("scheme appears with empty host")
}
} else if _, err = ParseHost(uu.Host); err != nil {
return nil, err
}
// Clean path in the URL.
// Note: path.Clean() is used on purpose because in MS Windows filepath.Clean() converts
// `/` into `\` ie `/foo` becomes `\foo`
if uu.Path != "" {
uu.Path = path.Clean(uu.Path)
}
v := URL(*uu)
u = &v
return u, nil
}

167
pkg/net/url_test.go Normal file
View File

@@ -0,0 +1,167 @@
/*
* Minio Cloud Storage, (C) 2018 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 net
import (
"reflect"
"testing"
)
func TestURLIsEmpty(t *testing.T) {
testCases := []struct {
url URL
expectedResult bool
}{
{URL{}, true},
{URL{Scheme: "http", Host: "play"}, false},
{URL{Path: "path/to/play"}, false},
}
for i, testCase := range testCases {
result := testCase.url.IsEmpty()
if result != testCase.expectedResult {
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestURLString(t *testing.T) {
testCases := []struct {
url URL
expectedStr string
}{
{URL{}, ""},
{URL{Scheme: "http", Host: "play"}, "http://play"},
{URL{Scheme: "https", Host: "play:443"}, "https://play"},
{URL{Scheme: "https", Host: "play.minio.io:80"}, "https://play.minio.io:80"},
{URL{Scheme: "https", Host: "147.75.201.93:9000", Path: "/"}, "https://147.75.201.93:9000/"},
{URL{Scheme: "https", Host: "s3.amazonaws.com", Path: "/", RawQuery: "location"}, "https://s3.amazonaws.com/?location"},
{URL{Scheme: "http", Host: "myminio:10000", Path: "/mybucket/myobject"}, "http://myminio:10000/mybucket/myobject"},
{URL{Scheme: "ftp", Host: "myftp.server:10000", Path: "/myuser"}, "ftp://myftp.server:10000/myuser"},
{URL{Path: "path/to/play"}, "path/to/play"},
}
for i, testCase := range testCases {
str := testCase.url.String()
if str != testCase.expectedStr {
t.Fatalf("test %v: string: expected: %v, got: %v", i+1, testCase.expectedStr, str)
}
}
}
func TestURLMarshalJSON(t *testing.T) {
testCases := []struct {
url URL
expectedData []byte
expectErr bool
}{
{URL{}, []byte(`""`), false},
{URL{Scheme: "http", Host: "play"}, []byte(`"http://play"`), false},
{URL{Scheme: "https", Host: "play.minio.io:0"}, []byte(`"https://play.minio.io:0"`), false},
{URL{Scheme: "https", Host: "147.75.201.93:9000", Path: "/"}, []byte(`"https://147.75.201.93:9000/"`), false},
{URL{Scheme: "https", Host: "s3.amazonaws.com", Path: "/", RawQuery: "location"}, []byte(`"https://s3.amazonaws.com/?location"`), false},
{URL{Scheme: "http", Host: "myminio:10000", Path: "/mybucket/myobject"}, []byte(`"http://myminio:10000/mybucket/myobject"`), false},
{URL{Scheme: "ftp", Host: "myftp.server:10000", Path: "/myuser"}, []byte(`"ftp://myftp.server:10000/myuser"`), false},
}
for i, testCase := range testCases {
data, err := testCase.url.MarshalJSON()
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(data, testCase.expectedData) {
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, string(testCase.expectedData), string(data))
}
}
}
}
func TestURLUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedURL *URL
expectErr bool
}{
{[]byte(`""`), &URL{}, false},
{[]byte(`"http://play"`), &URL{Scheme: "http", Host: "play"}, false},
{[]byte(`"https://play.minio.io:0"`), &URL{Scheme: "https", Host: "play.minio.io:0"}, false},
{[]byte(`"https://147.75.201.93:9000/"`), &URL{Scheme: "https", Host: "147.75.201.93:9000", Path: "/"}, false},
{[]byte(`"https://s3.amazonaws.com/?location"`), &URL{Scheme: "https", Host: "s3.amazonaws.com", Path: "/", RawQuery: "location"}, false},
{[]byte(`"http://myminio:10000/mybucket//myobject/"`), &URL{Scheme: "http", Host: "myminio:10000", Path: "/mybucket/myobject"}, false},
{[]byte(`"ftp://myftp.server:10000/myuser"`), &URL{Scheme: "ftp", Host: "myftp.server:10000", Path: "/myuser"}, false},
{[]byte(`"myserver:1000"`), nil, true},
{[]byte(`"http://:1000/mybucket"`), nil, true},
{[]byte(`"https://147.75.201.93:90000/"`), nil, true},
{[]byte(`"http:/play"`), nil, true},
}
for i, testCase := range testCases {
var url URL
err := url.UnmarshalJSON(testCase.data)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(&url, testCase.expectedURL) {
t.Fatalf("test %v: host: expected: %#v, got: %#v", i+1, testCase.expectedURL, url)
}
}
}
}
func TestParseURL(t *testing.T) {
testCases := []struct {
s string
expectedURL *URL
expectErr bool
}{
{"http://play", &URL{Scheme: "http", Host: "play"}, false},
{"https://play.minio.io:0", &URL{Scheme: "https", Host: "play.minio.io:0"}, false},
{"https://147.75.201.93:9000/", &URL{Scheme: "https", Host: "147.75.201.93:9000", Path: "/"}, false},
{"https://s3.amazonaws.com/?location", &URL{Scheme: "https", Host: "s3.amazonaws.com", Path: "/", RawQuery: "location"}, false},
{"http://myminio:10000/mybucket//myobject/", &URL{Scheme: "http", Host: "myminio:10000", Path: "/mybucket/myobject"}, false},
{"ftp://myftp.server:10000/myuser", &URL{Scheme: "ftp", Host: "myftp.server:10000", Path: "/myuser"}, false},
{"myserver:1000", nil, true},
{"http://:1000/mybucket", nil, true},
{"https://147.75.201.93:90000/", nil, true},
{"http:/play", nil, true},
}
for i, testCase := range testCases {
url, err := ParseURL(testCase.s)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(url, testCase.expectedURL) {
t.Fatalf("test %v: host: expected: %#v, got: %#v", i+1, testCase.expectedURL, url)
}
}
}
}