diff --git a/.travis.yml b/.travis.yml index 2da32a1..f45dddc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -language: generic +language: bash sudo: required dist: trusty @@ -11,7 +11,26 @@ install: - docker build -t cameradar . script: -- docker run cameradar +- go get github.com/andelf/go-curl +- go get github.com/pkg/errors +- go get gopkg.in/go-playground/validator.v9 +- go get github.com/stretchr/testify/assert +# Run unit tests +- go test +# Launch a fake camera to check if cameradar is able to access it +- docker run -d --name=fake_camera -e RTSP_USERNAME=admin -e RTSP_PASSWORD=12345 -p 8554:8554 ullaakut/rtspatt +# Launch cameradar on the local machine +- docker run --net=host -t cameradar -t 0.0.0.0 -l > logs.txt +- docker logs fake_camera > camera_logs.txt +# Stop the fake camera +- docker stop fake_camera +# Print logs +- cat camera_logs.txt +- cat logs.txt +# check if file contains more than one line +# 1 line: Error message because no streams were found +# More lines: Logs for all found cameras +- if [[ $(wc -l 0 { + if err == nil { + fmt.Printf("unexpected success in AttackCredentials test, iteration %d. expected error: %s\n", i, vector.expectedErrMsg) + os.Exit(1) + } + assert.Contains(t, err.Error(), vector.expectedErrMsg, "wrong error message") + } else { + if err != nil { + fmt.Printf("unexpected error in AttackCredentials test, iteration %d: %v\n", i, err) + os.Exit(1) + } + for _, stream := range vector.expectedStreams { + foundStream := false + for _, result := range results { + if result.Address == stream.Address && result.Device == stream.Device && result.Port == stream.Port { + foundStream = true + } + } + assert.Equal(t, true, foundStream, "wrong streams parsed") + } + } + assert.Equal(t, len(vector.expectedStreams), len(results), "wrong streams parsed") + + } +} + +func TestAttackRoute(t *testing.T) { + validStream1 := Stream{ + Device: "fakeDevice", + Address: "fakeAddress", + Port: 1337, + } + + validStream2 := Stream{ + Device: "fakeDevice", + Address: "differentFakeAddress", + Port: 1337, + } + + fakeTargets := []Stream{validStream1, validStream2} + fakeRoutes := Routes{"live.sdp", "media.amp"} + + vectors := []struct { + targets []Stream + routes Routes + timeout time.Duration + log bool + + expectedStreams []Stream + expectedErrMsg string + }{ + // Valid baseline + { + targets: fakeTargets, + routes: fakeRoutes, + timeout: 1 * time.Millisecond, + log: true, + + expectedStreams: fakeTargets, + }, + // Valid baseline without logs + { + targets: fakeTargets, + routes: fakeRoutes, + timeout: 1 * time.Millisecond, + log: false, + + expectedStreams: fakeTargets, + }, + // TODO: Refacto and make tests with all possible error cases + } + for i, vector := range vectors { + results, err := AttackRoute(vector.targets, vector.routes, vector.timeout, vector.log) + + if len(vector.expectedErrMsg) > 0 { + if err == nil { + fmt.Printf("unexpected success in AttackRoute test, iteration %d. expected error: %s\n", i, vector.expectedErrMsg) + os.Exit(1) + } + assert.Contains(t, err.Error(), vector.expectedErrMsg, "wrong error message") + } else { + if err != nil { + fmt.Printf("unexpected error in AttackRoute test, iteration %d: %v\n", i, err) + os.Exit(1) + } + for _, stream := range vector.expectedStreams { + foundStream := false + for _, result := range results { + if result.Address == stream.Address && result.Device == stream.Device && result.Port == stream.Port { + foundStream = true + } + } + assert.Equal(t, true, foundStream, "wrong streams parsed") + } + } + assert.Equal(t, len(vector.expectedStreams), len(results), "wrong streams parsed") + } +} diff --git a/discover.go b/discover.go index 914c64c..1fb9f37 100644 --- a/discover.go +++ b/discover.go @@ -17,7 +17,6 @@ import ( "encoding/xml" "fmt" "io/ioutil" - "log" "os/exec" "github.com/pkg/errors" @@ -27,7 +26,7 @@ import ( // These constants detail the different level of nmap speed presets // that determine the timeout values and wether or not nmap makes use of parallelism const ( - // PARANOID NO PARALLELISM | 5min timeout | 100ms to 10s round-trip time timeout | 5mn scan delay + // PARANOIAC NO PARALLELISM | 5min timeout | 100ms to 10s round-trip time timeout | 5mn scan delay PARANOIAC = 0 // SNEAKY NO PARALLELISM | 15sec timeout | 100ms to 10s round-trip time timeout | 15s scan delay SNEAKY = 1 @@ -41,10 +40,19 @@ const ( INSANE = 5 ) +// Allows unit tests to override the exec function to avoid launching a real command +// during the tests. The NmapRun method will soon be refactored with an adaptor in order +// to make it possible to mock all external calls. +var execCommand = exec.Command + // NmapRun runs nmap on the specified targets's specified ports, using the given nmap speed. func NmapRun(targets, ports, resultFilePath string, nmapSpeed int, enableLogs bool) error { + if nmapSpeed < PARANOIAC || nmapSpeed > INSANE { + return fmt.Errorf("invalid nmap speed value '%d'. Should be between '%d' and '%d'", nmapSpeed, PARANOIAC, INSANE) + } + // Prepare nmap command - cmd := exec.Command( + cmd := execCommand( "nmap", fmt.Sprintf("-T%d", nmapSpeed), "-A", @@ -58,24 +66,24 @@ func NmapRun(targets, ports, resultFilePath string, nmapSpeed int, enableLogs bo // Pipe stdout to be able to write the logs in realtime stdout, err := cmd.StdoutPipe() if err != nil { - return errors.Wrap(err, "Couldn't get stdout pipe") + return errors.Wrap(err, "couldn't get stdout pipe") } // Execute the nmap command if err := cmd.Start(); err != nil { - return errors.Wrap(err, "Coudln't run nmap command") + return errors.Wrap(err, "coudln't run nmap command") } // Scan the pipe until an end of file or an error occurs in := bufio.NewScanner(stdout) for in.Scan() { if enableLogs { - log.Printf(in.Text()) + fmt.Printf(in.Text()) } } if err := in.Err(); err != nil { if enableLogs { - log.Printf("error: %s", err) + fmt.Printf("error: %s\n", err) } } @@ -90,7 +98,7 @@ func NmapParseResults(nmapResultFilePath string) ([]Stream, error) { // Open & Read XML file content, err := ioutil.ReadFile(nmapResultFilePath) if err != nil { - return streams, errors.Wrap(err, "Could not read nmap result file at "+nmapResultFilePath+":") + return streams, errors.Wrap(err, "could not read nmap result file at "+nmapResultFilePath+":") } // Unmarshal content of XML file into data structure diff --git a/discover_test.go b/discover_test.go new file mode 100644 index 0000000..d3522c3 --- /dev/null +++ b/discover_test.go @@ -0,0 +1,715 @@ +// 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 cmrdr + +import ( + "encoding/xml" + "fmt" + "io/ioutil" + "os" + "os/exec" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +// HACK: See https://golang.org/src/os/exec/exec_test.go +func fakeExecCommand(command string, args ...string) *exec.Cmd { + cs := []string{"-test.run=TestExecCommandHelper", "--", command} + cs = append(cs, args...) + cmd := exec.Command(os.Args[0], cs...) + cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1", + "STDOUT= ", + "EXIT_STATUS=0"} + return cmd +} + +func TestExecCommandHelper(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + + fmt.Fprintf(os.Stdout, os.Getenv("STDOUT")) + i, _ := strconv.Atoi(os.Getenv("EXIT_STATUS")) + os.Exit(i) +} + +func TestNmapRun(t *testing.T) { + execCommand = fakeExecCommand + defer func() { execCommand = exec.Command }() + + vectors := []struct { + targets string + ports string + resultFilePath string + nmapSpeed int + enableLogs bool + + expectedErrMsg string + }{ + // Valid baseline with logs enabled + { + targets: "localhost", + ports: "554", + resultFilePath: "/tmp/results.xml", + nmapSpeed: PARANOIAC, + enableLogs: true, + }, + // Invalid speed + { + targets: "localhost", + ports: "554", + resultFilePath: "/tmp/results.xml", + nmapSpeed: INSANE + 1, + enableLogs: false, + + expectedErrMsg: "invalid nmap speed value", + }, + } + for _, vector := range vectors { + err := NmapRun(vector.targets, vector.ports, vector.resultFilePath, vector.nmapSpeed, vector.enableLogs) + if len(vector.expectedErrMsg) > 0 { + if err == nil { + fmt.Printf("unexpected success. expected error: %s\n", vector.expectedErrMsg) + os.Exit(1) + } + assert.Contains(t, err.Error(), vector.expectedErrMsg, "wrong error message") + } else { + if err != nil { + fmt.Printf("unexpected error: %v\n", err) + os.Exit(1) + } + } + } +} + +func TestNmapParseResults(t *testing.T) { + validStream1 := Stream{ + Device: "fakeDevice", + Address: "fakeAddress", + Port: 1337, + } + + validStream2 := Stream{ + Device: "fakeDevice", + Address: "differentFakeAddress", + Port: 1337, + } + + invalidStreamNoPort := Stream{ + Device: "invalidDevice", + Address: "fakeAddress", + Port: 0, + } + + invalidStreamNoAddress := Stream{ + Device: "invalidDevice", + Address: "", + Port: 1337, + } + + vectors := []struct { + fileExists bool + streamsXML *nmapResult + + expectedStreams []Stream + expectedErrMsg string + }{ + // File exists + // Two valid streams, no error + { + expectedStreams: []Stream{validStream1, validStream2}, + streamsXML: &nmapResult{ + Hosts: []host{ + host{ + Address: address{ + Addr: validStream1.Address, + AddrType: "ipv4", + }, + Ports: ports{ + Ports: []port{ + port{ + PortID: validStream1.Port, + State: state{ + State: "open", + }, + Service: service{ + Name: "rtsp", + Product: validStream1.Device, + }, + }, + }, + }, + }, + host{ + Address: address{ + Addr: validStream2.Address, + AddrType: "ipv4", + }, + Ports: ports{ + Ports: []port{ + port{ + PortID: validStream2.Port, + State: state{ + State: "open", + }, + Service: service{ + Name: "rtsp", + Product: validStream2.Device, + }, + }, + }, + }, + }, + }, + }, + fileExists: true, + }, + // File exists + // Two invalid streams, no error + { + fileExists: true, + expectedStreams: []Stream{invalidStreamNoPort, invalidStreamNoAddress}, + streamsXML: &nmapResult{ + Hosts: []host{ + host{ + Address: address{ + Addr: invalidStreamNoAddress.Address, + AddrType: "ipv4", + }, + Ports: ports{ + Ports: []port{ + port{ + PortID: invalidStreamNoAddress.Port, + State: state{ + State: "open", + }, + Service: service{ + Name: "rtsp", + Product: invalidStreamNoAddress.Device, + }, + }, + }, + }, + }, + host{ + Address: address{ + Addr: invalidStreamNoPort.Address, + AddrType: "ipv4", + }, + Ports: ports{ + Ports: []port{ + port{ + PortID: invalidStreamNoPort.Port, + State: state{ + State: "open", + }, + Service: service{ + Name: "rtsp", + Product: invalidStreamNoPort.Device, + }, + }, + }, + }, + }, + }, + }, + }, + // File does not exist, error + { + fileExists: false, + expectedErrMsg: "could not read nmap result file", + }, + // No valid streams found + { + fileExists: true, + expectedStreams: []Stream{}, + streamsXML: &nmapResult{ + Hosts: []host{ + host{ + Address: address{ + Addr: "Camera with closed ports", + AddrType: "ipv4", + }, + Ports: ports{ + Ports: []port{ + port{ + PortID: 0, + State: state{ + State: "closed", + }, + Service: service{ + Name: "rtsp", + Product: "Camera without closed ports", + }, + }, + }, + }, + }, + host{ + Address: address{ + Addr: "Camera with closed ports", + AddrType: "ipv4", + }, + }, + }, + }, + }, + // XML Unmarshal error + { + fileExists: true, + expectedStreams: []Stream{}, + expectedErrMsg: "expected element type but have ", + }, + } + for i, vector := range vectors { + filePath := "/tmp/cameradar_test_parse_results_" + fmt.Sprint(i) + ".xml" + + // create file + if vector.fileExists { + _, err := os.Create(filePath) + if err != nil { + fmt.Printf("could not create xml file for NmapParseResults: %v. iteration: %d. file path: %s\n", err, i, filePath) + os.Exit(1) + } + + // marshal and write + if vector.streamsXML != nil { + streams, err := xml.Marshal(vector.streamsXML) + if err != nil { + fmt.Printf("invalid streams for NmapParseResults: %v. iteration: %d. streams: %v\n", err, i, vector.streamsXML) + os.Exit(1) + } + + err = ioutil.WriteFile(filePath, streams, 0644) + if err != nil { + fmt.Printf("could not write xml file for NmapParseResults: %v. iteration: %d. file path: %s\n", err, i, filePath) + os.Exit(1) + } + } else { + err := ioutil.WriteFile(filePath, []byte(""), 0644) + if err != nil { + fmt.Printf("could not write xml file for NmapParseResults: %v. iteration: %d. file path: %s\n", err, i, filePath) + os.Exit(1) + } + } + } + + results, err := NmapParseResults(filePath) + if len(vector.expectedErrMsg) > 0 { + if err == nil { + fmt.Printf("unexpected success. expected error: %s\n", vector.expectedErrMsg) + os.Exit(1) + } + assert.Contains(t, err.Error(), vector.expectedErrMsg, "wrong error message") + } else { + if err != nil { + fmt.Printf("unexpected error: %v\n", err) + os.Exit(1) + } + for _, stream := range vector.expectedStreams { + foundStream := false + for _, result := range results { + if result.Address == stream.Address && result.Device == stream.Device && result.Port == stream.Port { + foundStream = true + } + } + assert.Equal(t, true, foundStream, "wrong streams parsed") + } + } + assert.Equal(t, len(vector.expectedStreams), len(results), "wrong streams parsed") + } +} + +func TestDiscover(t *testing.T) { + execCommand = fakeExecCommand + defer func() { execCommand = exec.Command }() + + validStream1 := Stream{ + Device: "fakeDevice", + Address: "fakeAddress", + Port: 1337, + } + + validStream2 := Stream{ + Device: "fakeDevice", + Address: "differentFakeAddress", + Port: 1337, + } + + invalidStreamNoPort := Stream{ + Device: "invalidDevice", + Address: "fakeAddress", + Port: 0, + } + + invalidStreamNoAddress := Stream{ + Device: "invalidDevice", + Address: "", + Port: 1337, + } + + vectors := []struct { + targets string + ports string + resultFilePath string + nmapSpeed int + enableLogs bool + fileExists bool + streamsXML *nmapResult + + expectedStreams []Stream + expectedErrMsg string + }{ + // Valid baseline + { + expectedStreams: []Stream{validStream1, validStream2}, + streamsXML: &nmapResult{ + Hosts: []host{ + host{ + Address: address{ + Addr: validStream1.Address, + AddrType: "ipv4", + }, + Ports: ports{ + Ports: []port{ + port{ + PortID: validStream1.Port, + State: state{ + State: "open", + }, + Service: service{ + Name: "rtsp", + Product: validStream1.Device, + }, + }, + }, + }, + }, + host{ + Address: address{ + Addr: validStream2.Address, + AddrType: "ipv4", + }, + Ports: ports{ + Ports: []port{ + port{ + PortID: validStream2.Port, + State: state{ + State: "open", + }, + Service: service{ + Name: "rtsp", + Product: validStream2.Device, + }, + }, + }, + }, + }, + }, + }, + fileExists: true, + targets: "localhost", + ports: "554", + resultFilePath: "/tmp/results.xml", + nmapSpeed: PARANOIAC, + enableLogs: false, + }, + // Invalid speed + { + expectedStreams: []Stream{}, + streamsXML: &nmapResult{ + Hosts: []host{ + host{ + Address: address{ + Addr: validStream1.Address, + AddrType: "ipv4", + }, + Ports: ports{ + Ports: []port{ + port{ + PortID: validStream1.Port, + State: state{ + State: "open", + }, + Service: service{ + Name: "rtsp", + Product: validStream1.Device, + }, + }, + }, + }, + }, + host{ + Address: address{ + Addr: validStream2.Address, + AddrType: "ipv4", + }, + Ports: ports{ + Ports: []port{ + port{ + PortID: validStream2.Port, + State: state{ + State: "open", + }, + Service: service{ + Name: "rtsp", + Product: validStream2.Device, + }, + }, + }, + }, + }, + }, + }, + fileExists: true, + targets: "localhost", + ports: "554", + resultFilePath: "/tmp/results.xml", + nmapSpeed: INSANE + 1, + enableLogs: false, + + expectedErrMsg: "invalid nmap speed value", + }, + // File exists + // Two valid streams, no error + { + expectedStreams: []Stream{validStream1, validStream2}, + streamsXML: &nmapResult{ + Hosts: []host{ + host{ + Address: address{ + Addr: validStream1.Address, + AddrType: "ipv4", + }, + Ports: ports{ + Ports: []port{ + port{ + PortID: validStream1.Port, + State: state{ + State: "open", + }, + Service: service{ + Name: "rtsp", + Product: validStream1.Device, + }, + }, + }, + }, + }, + host{ + Address: address{ + Addr: validStream2.Address, + AddrType: "ipv4", + }, + Ports: ports{ + Ports: []port{ + port{ + PortID: validStream2.Port, + State: state{ + State: "open", + }, + Service: service{ + Name: "rtsp", + Product: validStream2.Device, + }, + }, + }, + }, + }, + }, + }, + fileExists: true, + targets: "localhost", + ports: "554", + resultFilePath: "/tmp/results.xml", + nmapSpeed: PARANOIAC, + enableLogs: false, + }, + // File exists + // Two invalid streams, no error + { + fileExists: true, + expectedStreams: []Stream{invalidStreamNoPort, invalidStreamNoAddress}, + streamsXML: &nmapResult{ + Hosts: []host{ + host{ + Address: address{ + Addr: invalidStreamNoAddress.Address, + AddrType: "ipv4", + }, + Ports: ports{ + Ports: []port{ + port{ + PortID: invalidStreamNoAddress.Port, + State: state{ + State: "open", + }, + Service: service{ + Name: "rtsp", + Product: invalidStreamNoAddress.Device, + }, + }, + }, + }, + }, + host{ + Address: address{ + Addr: invalidStreamNoPort.Address, + AddrType: "ipv4", + }, + Ports: ports{ + Ports: []port{ + port{ + PortID: invalidStreamNoPort.Port, + State: state{ + State: "open", + }, + Service: service{ + Name: "rtsp", + Product: invalidStreamNoPort.Device, + }, + }, + }, + }, + }, + }, + }, + targets: "localhost", + ports: "554", + resultFilePath: "/tmp/results.xml", + nmapSpeed: PARANOIAC, + enableLogs: false, + }, + // File does not exist, error + { + fileExists: false, + expectedErrMsg: "could not read nmap result file", + targets: "localhost", + ports: "554", + resultFilePath: "/tmp/results.xml", + nmapSpeed: PARANOIAC, + enableLogs: false, + }, + // No valid streams found + { + fileExists: true, + expectedStreams: []Stream{}, + streamsXML: &nmapResult{ + Hosts: []host{ + host{ + Address: address{ + Addr: "Camera with closed ports", + AddrType: "ipv4", + }, + Ports: ports{ + Ports: []port{ + port{ + PortID: 0, + State: state{ + State: "closed", + }, + Service: service{ + Name: "rtsp", + Product: "Camera without closed ports", + }, + }, + }, + }, + }, + host{ + Address: address{ + Addr: "Camera with closed ports", + AddrType: "ipv4", + }, + }, + }, + }, + targets: "localhost", + ports: "554", + resultFilePath: "/tmp/results.xml", + nmapSpeed: PARANOIAC, + enableLogs: false, + }, + // XML Unmarshal error + { + fileExists: true, + expectedStreams: []Stream{}, + expectedErrMsg: "expected element type but have ", + targets: "localhost", + ports: "554", + resultFilePath: "/tmp/results.xml", + nmapSpeed: PARANOIAC, + enableLogs: false, + }, + } + for i, vector := range vectors { + filePath := "/tmp/cameradar_test_discover_" + fmt.Sprint(i) + ".xml" + + // create file + if vector.fileExists { + _, err := os.Create(filePath) + if err != nil { + fmt.Printf("could not create xml file for Discover: %v. iteration: %d. file path: %s\n", err, i, filePath) + os.Exit(1) + } + + // marshal and write + if vector.streamsXML != nil { + streams, err := xml.Marshal(vector.streamsXML) + if err != nil { + fmt.Printf("invalid streams for Discover: %v. iteration: %d. streams: %v\n", err, i, vector.streamsXML) + os.Exit(1) + } + + err = ioutil.WriteFile(filePath, streams, 0644) + if err != nil { + fmt.Printf("could not write xml file for Discover: %v. iteration: %d. file path: %s\n", err, i, filePath) + os.Exit(1) + } + } else { + err := ioutil.WriteFile(filePath, []byte(""), 0644) + if err != nil { + fmt.Printf("could not write xml file for Discover: %v. iteration: %d. file path: %s\n", err, i, filePath) + os.Exit(1) + } + } + } + + results, err := Discover(vector.targets, vector.ports, filePath, vector.nmapSpeed, vector.enableLogs) + + if len(vector.expectedErrMsg) > 0 { + if err == nil { + fmt.Printf("unexpected success in Discover test, iteration %d. expected error: %s\n", i, vector.expectedErrMsg) + os.Exit(1) + } + assert.Contains(t, err.Error(), vector.expectedErrMsg, "wrong error message") + } else { + if err != nil { + fmt.Printf("unexpected error in Discover test, iteration %d: %v\n", i, err) + os.Exit(1) + } + for _, stream := range vector.expectedStreams { + foundStream := false + for _, result := range results { + if result.Address == stream.Address && result.Device == stream.Device && result.Port == stream.Port { + foundStream = true + } + } + assert.Equal(t, true, foundStream, "wrong streams parsed") + } + } + assert.Equal(t, len(vector.expectedStreams), len(results), "wrong streams parsed") + } +} diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 0000000..5efb3a1 --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,123 @@ +// 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 cmrdr + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReplace(t *testing.T) { + validStream1 := Stream{ + Device: "fakeDevice", + Address: "fakeAddress", + Port: 1337, + } + + validStream2 := Stream{ + Device: "fakeDevice", + Address: "differentFakeAddress", + Port: 1337, + } + + invalidStreamNoPort := Stream{ + Device: "invalidDevice", + Address: "fakeAddress", + Port: 0, + } + + invalidStreamNoPortModified := Stream{ + Device: "updatedDevice", + Address: "fakeAddress", + Port: 1337, + } + + vectors := []struct { + streams []Stream + newStream Stream + + expectedStreams []Stream + }{ + // Valid baseline + { + streams: []Stream{validStream1, validStream2, invalidStreamNoPort}, + newStream: invalidStreamNoPortModified, + + expectedStreams: []Stream{validStream1, validStream2, invalidStreamNoPortModified}, + }, + } + for _, vector := range vectors { + streams := replace(vector.streams, vector.newStream) + + for _, stream := range vector.streams { + foundStream := false + for _, result := range streams { + if result.Address == stream.Address && result.Device == stream.Device && result.Port == stream.Port { + foundStream = true + } + } + assert.Equal(t, true, foundStream, "wrong streams parsed") + } + } +} + +func TestGetCameraRTSPURL(t *testing.T) { + validStream := Stream{ + Address: "1.2.3.4", + Username: "ullaakut", + Password: "ba69897483886f0d2b0afb6345b76c0c", + Route: "cameradar.sdp", + Port: 1337, + } + + vectors := []struct { + stream Stream + + expectedRTSPURL string + }{ + // Valid baseline + { + stream: validStream, + + expectedRTSPURL: "rtsp://ullaakut:ba69897483886f0d2b0afb6345b76c0c@1.2.3.4:1337/cameradar.sdp", + }, + } + for _, vector := range vectors { + output := GetCameraRTSPURL(vector.stream) + assert.Equal(t, vector.expectedRTSPURL, output, "wrong RTSP URL generated") + } +} + +func TestGetCameraAdminPanelURL(t *testing.T) { + validStream := Stream{ + Address: "1.2.3.4", + } + + vectors := []struct { + stream Stream + + expectedRTSPURL string + }{ + // Valid baseline + { + stream: validStream, + + expectedRTSPURL: "http://1.2.3.4/", + }, + } + for _, vector := range vectors { + output := GetCameraAdminPanelURL(vector.stream) + assert.Equal(t, vector.expectedRTSPURL, output, "wrong Admin Panel URL generated") + } +} diff --git a/loaders.go b/loaders.go index 5b88101..d506c80 100644 --- a/loaders.go +++ b/loaders.go @@ -28,7 +28,7 @@ func LoadCredentials(path string) (Credentials, error) { // Open & Read XML file content, err := ioutil.ReadFile(path) if err != nil { - return creds, errors.Wrap(err, "Could not read credentials dictionary file at "+path+":") + return creds, errors.Wrap(err, "could not read credentials dictionary file at "+path+":") } // Unmarshal content of JSON file into data structure diff --git a/loaders_test.go b/loaders_test.go new file mode 100644 index 0000000..e344d28 --- /dev/null +++ b/loaders_test.go @@ -0,0 +1,182 @@ +// 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 cmrdr + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoadCredentials(t *testing.T) { + credentialsJSONString := []byte("{\"usernames\":[\"admin\",\"root\"],\"passwords\":[\"12345\",\"root\"]}") + validCredentials := Credentials{ + Usernames: []string{"admin", "root"}, + Passwords: []string{"12345", "root"}, + } + + vectors := []struct { + input []byte + fileExists bool + + expectedOutput Credentials + expectedErrMsg string + }{ + // Valid baseline + { + fileExists: true, + input: credentialsJSONString, + expectedOutput: validCredentials, + }, + // File does not exist + { + fileExists: false, + input: credentialsJSONString, + expectedErrMsg: "could not read credentials dictionary file at", + }, + // Invalid format + { + fileExists: true, + input: []byte("not json"), + expectedErrMsg: "invalid character", + }, + // No streams in dictionary + { + fileExists: true, + input: []byte("{\"invalid\":\"json\"}"), + }, + } + for i, vector := range vectors { + filePath := "/tmp/cameradar_test_load_credentials_" + fmt.Sprint(i) + ".xml" + // create file + if vector.fileExists { + _, err := os.Create(filePath) + if err != nil { + fmt.Printf("could not create xml file for LoadCredentials: %v. iteration: %d. file path: %s\n", err, i, filePath) + os.Exit(1) + } + + err = ioutil.WriteFile(filePath, vector.input, 0644) + if err != nil { + fmt.Printf("could not write xml file for LoadCredentials: %v. iteration: %d. file path: %s\n", err, i, filePath) + os.Exit(1) + } + } + + result, err := LoadCredentials(filePath) + if len(vector.expectedErrMsg) > 0 { + if err == nil { + fmt.Printf("unexpected success in LoadCredentials test, iteration %d. expected error: %s\n", i, vector.expectedErrMsg) + os.Exit(1) + } + assert.Contains(t, err.Error(), vector.expectedErrMsg, "wrong error message") + } else { + if err != nil { + fmt.Printf("unexpected error in LoadCredentials test, iteration %d: %v\n", i, err) + os.Exit(1) + } + for _, expectedUsername := range vector.expectedOutput.Usernames { + foundUsername := false + for _, username := range result.Usernames { + if username == expectedUsername { + foundUsername = true + } + } + assert.Equal(t, true, foundUsername, "wrong usernames parsed") + } + for _, expectedPassword := range vector.expectedOutput.Passwords { + foundPassword := false + for _, password := range result.Passwords { + if password == expectedPassword { + foundPassword = true + } + } + assert.Equal(t, true, foundPassword, "wrong passwords parsed") + } + } + } +} + +func TestLoadRoutes(t *testing.T) { + routesJSONString := []byte("admin\nroot") + validRoutes := Routes{"admin", "root"} + + vectors := []struct { + input []byte + fileExists bool + + expectedOutput Routes + expectedErrMsg string + }{ + // Valid baseline + { + fileExists: true, + input: routesJSONString, + expectedOutput: validRoutes, + }, + // File does not exist + { + fileExists: false, + input: routesJSONString, + expectedErrMsg: "no such file or directory", + }, + // No streams in dictionary + { + fileExists: true, + input: []byte(""), + }, + } + for i, vector := range vectors { + filePath := "/tmp/cameradar_test_load_routes_" + fmt.Sprint(i) + ".xml" + // create file + if vector.fileExists { + _, err := os.Create(filePath) + if err != nil { + fmt.Printf("could not create xml file for LoadRoutes: %v. iteration: %d. file path: %s\n", err, i, filePath) + os.Exit(1) + } + + err = ioutil.WriteFile(filePath, vector.input, 0644) + if err != nil { + fmt.Printf("could not write xml file for LoadRoutes: %v. iteration: %d. file path: %s\n", err, i, filePath) + os.Exit(1) + } + } + + result, err := LoadRoutes(filePath) + if len(vector.expectedErrMsg) > 0 { + if err == nil { + fmt.Printf("unexpected success in LoadRoutes test, iteration %d. expected error: %s\n", i, vector.expectedErrMsg) + os.Exit(1) + } + assert.Contains(t, err.Error(), vector.expectedErrMsg, "wrong error message") + } else { + if err != nil { + fmt.Printf("unexpected error in LoadRoutes test, iteration %d: %v\n", i, err) + os.Exit(1) + } + for _, expectedRoute := range vector.expectedOutput { + foundRoute := false + for _, route := range result { + if route == expectedRoute { + foundRoute = true + } + } + assert.Equal(t, true, foundRoute, "wrong routes parsed") + } + } + } +}