diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/README.md b/README.md index 0a3e3de..d41e40e 100644 --- a/README.md +++ b/README.md @@ -16,41 +16,21 @@ To build, configure, and test the Roku WebDriver and Roku Robot Framework Librar 1. [Download](https://golang.org/dl/) and install the Go programming language (the Roku WebDriver server is implemented as a Go application). +1. Clone this repository or download it as a zip file. -2. Clone this repository or download it as a zip file. +1. Run the Roku WebDriver project: + ```bash + go run cmd/main.go + ``` -3. Set the "GOPATH" environment variable to the path of the **automated-channel-testing-master** folder ($APP_PATH). +1. Test the Roku WebDriver server following these steps: + ```bash + go test ./... + ``` -4. Install the following dependencies ([mux](https://github.com/gorilla/mux/blob/master/README.md) is a URL router and dispatcher; [logrus](https://github.com/sirupsen/logrus/blob/master/README.md) is a structured logger): - - cd /automated-channel-testing-master/src - go get github.com/gorilla/mux - go get github.com/sirupsen/logrus - -5. Build the Roku WebDriver project: - - go build main.go - -6. Run the **main** executable in the **/automated-channel-testing-master/src** folder to start the Roku WebDriver server. - - -7. Test the Roku WebDriver server following these steps: - - a. Install the [**assert**](https://godoc.org/github.com/stretchr/testify/assert) package, which provides testing tools to be used with Go applications. - - go get github.com/stretchr/testify/assert - - b. Test the ECP client: - - go test ecpClient - - c. Test the HTTP server (the host is "localhost"; the port used is 9000): - - go test httpServer - -8. Run Roku's Python-based sample WebDriver client application following these steps: +1. Run Roku's Python-based sample WebDriver client application following these steps: a. Download and install python: https://www.python.org/downloads. @@ -67,7 +47,7 @@ To build, configure, and test the Roku WebDriver and Roku Robot Framework Librar python /automated-channel-testing-master/sample/script/main.py -9. Configure and test the Roku Robot Framework Library following these steps: +1. Configure and test the Roku Robot Framework Library following these steps: a. Install the dependencies listed in the **/automated-channel-testing-master/RobotLibrary/requirements.txt** file: @@ -84,4 +64,4 @@ To build, configure, and test the Roku WebDriver and Roku Robot Framework Librar python -m robot.run --outputdir Results --variable ip_address:192.168.1.94 --variable server_path:D:/projects/go/webDriver/src/main.exe Tests/Basic_tests.robot -10. View the generated test case report and log, which are stored in the specified output directory. +1. View the generated test case report and log, which are stored in the specified output directory. diff --git a/src/main.go b/cmd/main.go similarity index 86% rename from src/main.go rename to cmd/main.go index 8993cb6..4f9a61b 100644 --- a/src/main.go +++ b/cmd/main.go @@ -16,10 +16,10 @@ package main import ( - httpServer "httpServer" + "github.com/rokudev/automated-channel-testing/pkg/httpServer" ) func main() { - server := httpServer.GetServerInstance() - server.Start() -} \ No newline at end of file + server := httpServer.GetServerInstance() + server.Start() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..41918c1 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/rokudev/automated-channel-testing + +go 1.14 + +require ( + github.com/gorilla/mux v1.7.4 + github.com/sirupsen/logrus v1.4.2 + github.com/stretchr/testify v1.5.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..62cc5d2 --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/src/ecpClient/ecp_client.go b/pkg/ecpClient/ecp_client.go similarity index 97% rename from src/ecpClient/ecp_client.go rename to pkg/ecpClient/ecp_client.go index 530bdd2..0a82155 100644 --- a/src/ecpClient/ecp_client.go +++ b/pkg/ecpClient/ecp_client.go @@ -16,13 +16,13 @@ package ecpClient import ( + "errors" "fmt" + "image" "io" "net/http" "net/url" "time" - "image" - "errors" ) const RequestTimeoutMilliseconds = 30000 @@ -79,7 +79,7 @@ func (ec *EcpClient) SetTimeout(timeout time.Duration) { } func (ec *EcpClient) GetTimeout() int { - ms := int(ec.HttpClient.HttpClient.Timeout/time.Millisecond) + ms := int(ec.HttpClient.HttpClient.Timeout / time.Millisecond) return ms } @@ -219,11 +219,11 @@ func (ec *EcpClient) InstallChannel(channelId string) (bool, error) { if err != nil { return false, err } - + return ec.makeNavigationRequest("POST", end) } -func (ec *EcpClient) LaunchChannel(channelId string, contentId string , mediaType string ) (bool, error) { +func (ec *EcpClient) LaunchChannel(channelId string, contentId string, mediaType string) (bool, error) { if len(channelId) == 0 { return false, errors.New("the channelId is required") } diff --git a/src/ecpClient/ecp_client_test.go b/pkg/ecpClient/ecp_client_test.go similarity index 67% rename from src/ecpClient/ecp_client_test.go rename to pkg/ecpClient/ecp_client_test.go index 9fbaf54..7d59bb6 100644 --- a/src/ecpClient/ecp_client_test.go +++ b/pkg/ecpClient/ecp_client_test.go @@ -16,11 +16,12 @@ package ecpClient import ( - "github.com/stretchr/testify/assert" + "encoding/xml" "net/http" "net/url" "testing" - "encoding/xml" + + "github.com/stretchr/testify/assert" ) var testRequest = &http.Request{Method: "GET", URL: base, Body: nil} @@ -159,9 +160,9 @@ func TestClient_CallActiveAppSuccessResponse(t *testing.T) { httpClient := GetMockedClient(&AppResponseMock) resp, err := httpClient.GetActiveApp() expectedResult := &App{ - Title: "test", - ID: "test", - Type: "test", + Title: "test", + ID: "test", + Type: "test", Version: "test", Subtype: "test", } @@ -181,16 +182,16 @@ func TestClient_CallAppsSuccessResponse(t *testing.T) { resp, err := httpClient.GetApps() expectedResult := &[]App{ App{ - Title: "test", - ID: "test", - Type: "test", + Title: "test", + ID: "test", + Type: "test", Version: "test", Subtype: "test", }, App{ - Title: "test2", - ID: "test2", - Type: "test2", + Title: "test2", + ID: "test2", + Type: "test2", Version: "test2", Subtype: "test2", }, @@ -210,65 +211,65 @@ func TestClient_CallDeviceInfoSuccessResponse(t *testing.T) { httpClient := GetMockedClient(&deviceInfoResponse) resp, err := httpClient.GetDeviceInfo() expectedResult := &DeviceInfo{ - Udn: "test", - SerialNumber: "test", - DeviceID: "test", - AdvertisingID: "test", - VendorName: "test", - ModelName: "test", - ModelNumber: "test", - ModelRegion: "test", - IsTv: "test", - IsStick: "test", - SupportsEthernet: "test", - WifiMac: "test", - WifiDriver: "test", - EthernetMac: "test", - NetworkType: "test", - NetworkName: "test", - FriendlyDeviceName: "test", - FriendlyModelName: "test", - DefaultDeviceName: "test", - UserDeviceName: "test", - UserDeviceLocation: "test", - BuildNumber: "test", - SoftwareVersion: "test", - SoftwareBuild: "test", - SecureDevice: "test", - Language: "test", - Country: "test", - Locale: "test", - TimeZoneAuto: "test", - TimeZone: "test", - TimeZoneName: "test", - TimeZoneTz: "test", - TimeZoneOffset: "test", - ClockFormat: "test", - Uptime: "test", - PowerMode: "test", - SupportsSuspend: "test", - SupportsFindRemote: "test", - FindRemoteIsPossible: "test", - SupportsAudioGuide: "test", - SupportsRva: "test", - DeveloperEnabled: "test", - KeyedDeveloperID: "test", - SearchEnabled: "test", - SearchChannelsEnabled: "test", - NotificationsEnabled: "test", - NotificationsFirstUse: "test", - SupportsPrivateListening: "test", - HeadphonesConnected: "test", - SupportsEcsTextedit: "test", - SupportsEcsMicrophone: "test", - SupportsWakeOnWlan: "test", - HasPlayOnRoku: "test", - HasMobileScreensaver: "test", - SupportURL: "test", - GrandcentralVersion: "test", - TrcVersion: "test", - TrcChannelVersion: "test", - DavinciVersion: "test", + Udn: "test", + SerialNumber: "test", + DeviceID: "test", + AdvertisingID: "test", + VendorName: "test", + ModelName: "test", + ModelNumber: "test", + ModelRegion: "test", + IsTv: "test", + IsStick: "test", + SupportsEthernet: "test", + WifiMac: "test", + WifiDriver: "test", + EthernetMac: "test", + NetworkType: "test", + NetworkName: "test", + FriendlyDeviceName: "test", + FriendlyModelName: "test", + DefaultDeviceName: "test", + UserDeviceName: "test", + UserDeviceLocation: "test", + BuildNumber: "test", + SoftwareVersion: "test", + SoftwareBuild: "test", + SecureDevice: "test", + Language: "test", + Country: "test", + Locale: "test", + TimeZoneAuto: "test", + TimeZone: "test", + TimeZoneName: "test", + TimeZoneTz: "test", + TimeZoneOffset: "test", + ClockFormat: "test", + Uptime: "test", + PowerMode: "test", + SupportsSuspend: "test", + SupportsFindRemote: "test", + FindRemoteIsPossible: "test", + SupportsAudioGuide: "test", + SupportsRva: "test", + DeveloperEnabled: "test", + KeyedDeveloperID: "test", + SearchEnabled: "test", + SearchChannelsEnabled: "test", + NotificationsEnabled: "test", + NotificationsFirstUse: "test", + SupportsPrivateListening: "test", + HeadphonesConnected: "test", + SupportsEcsTextedit: "test", + SupportsEcsMicrophone: "test", + SupportsWakeOnWlan: "test", + HasPlayOnRoku: "test", + HasMobileScreensaver: "test", + SupportURL: "test", + GrandcentralVersion: "test", + TrcVersion: "test", + TrcChannelVersion: "test", + DavinciVersion: "test", } assert.Nil(t, err) assert.Equal(t, expectedResult, resp) @@ -284,65 +285,65 @@ func TestClient_CallDeviceInfoNoResponse(t *testing.T) { func TestClient_CallAppUiSuccessResponse(t *testing.T) { httpClient := GetMockedClient(&UiResponseMock) resp, err := httpClient.GetAppUi() - expectedResult := &Node{ - XMLName: xml.Name{ - Local: "MainScene", - }, - Attrs: []xml.Attr{ - { - Name: xml.Name{ - Local: "bounds", - }, - Value: "{0, 0, 1920, 1080}", + expectedResult := &Node{ + XMLName: xml.Name{ + Local: "MainScene", + }, + Attrs: []xml.Attr{ + { + Name: xml.Name{ + Local: "bounds", }, - { - Name: xml.Name{ - Local: "children", - }, - Value: "0", + Value: "{0, 0, 1920, 1080}", + }, + { + Name: xml.Name{ + Local: "children", }, - { - Name: xml.Name{ - Local: "extends", - }, - Value: "BaseScene", + Value: "0", + }, + { + Name: xml.Name{ + Local: "extends", }, - { - Name: xml.Name{ - Local: "focusable", - }, - Value: "true", + Value: "BaseScene", + }, + { + Name: xml.Name{ + Local: "focusable", }, + Value: "true", }, - Nodes: []Node{ - { - XMLName: xml.Name{ - Local: "Poster", - }, - Attrs: []xml.Attr{ - { - Name: xml.Name{ - Local: "bounds", - }, - Value: "{0, 0, 1920, 1080}", + }, + Nodes: []Node{ + { + XMLName: xml.Name{ + Local: "Poster", + }, + Attrs: []xml.Attr{ + { + Name: xml.Name{ + Local: "bounds", }, - { - Name: xml.Name{ - Local: "index", - }, - Value: "0", + Value: "{0, 0, 1920, 1080}", + }, + { + Name: xml.Name{ + Local: "index", }, - { - Name: xml.Name{ - Local: "loadStatus", - }, - Value: "3", + Value: "0", + }, + { + Name: xml.Name{ + Local: "loadStatus", }, + Value: "3", }, - Nodes: nil, }, + Nodes: nil, }, - } + }, + } assert.Nil(t, err) assert.Equal(t, expectedResult, resp) } @@ -353,4 +354,3 @@ func TestClient_CallAppUiNoResponse(t *testing.T) { assert.NotNil(t, err) assert.Nil(t, resp) } - diff --git a/src/ecpClient/ecp_parser.go b/pkg/ecpClient/ecp_parser.go similarity index 99% rename from src/ecpClient/ecp_parser.go rename to pkg/ecpClient/ecp_parser.go index 409f006..a3c264b 100644 --- a/src/ecpClient/ecp_parser.go +++ b/pkg/ecpClient/ecp_parser.go @@ -16,11 +16,11 @@ package ecpClient import ( - "net/http" "encoding/xml" - "io/ioutil" - "image/png" "image" + "image/png" + "io/ioutil" + "net/http" ) func (ec *EcpClient) parseApps(res *http.Response) (*[]App, error) { @@ -81,7 +81,6 @@ func (ec *EcpClient) parseImage(res *http.Response) (image.Image, error) { return result, err } - func (ec *EcpClient) parseAppUiSource(res *http.Response) ([]byte, error) { data, err := ioutil.ReadAll(res.Body) if err != nil { @@ -100,4 +99,4 @@ func (ec *EcpClient) parseResponse(res *http.Response, v interface{}) (interface return nil, err } return v, err -} \ No newline at end of file +} diff --git a/src/ecpClient/http_client.go b/pkg/ecpClient/http_client.go similarity index 100% rename from src/ecpClient/http_client.go rename to pkg/ecpClient/http_client.go diff --git a/src/ecpClient/http_client_mock.go b/pkg/ecpClient/http_client_mock.go similarity index 97% rename from src/ecpClient/http_client_mock.go rename to pkg/ecpClient/http_client_mock.go index 69233db..71ccc2c 100644 --- a/src/ecpClient/http_client_mock.go +++ b/pkg/ecpClient/http_client_mock.go @@ -23,6 +23,7 @@ import ( ) var base, _ = url.Parse("some.api") + // RoundTripFunc . type RoundTripFunc func(req *http.Request) (*http.Response, error) @@ -45,17 +46,16 @@ func GetMockedClient(resp *string) *EcpClient { cli.HttpClient = httpClient contentsClient := EcpClient{ - BaseURL: base, - HttpClient: cli, + BaseURL: base, + HttpClient: cli, } return &contentsClient } - func TestingHTTPClient(resp *string) *http.Client { var function func(r *http.Request) (*http.Response, error) - if resp != nil{ + if resp != nil { function = func(req *http.Request) (*http.Response, error) { // Test request parameters return &http.Response{ diff --git a/src/ecpClient/response_mocks.go b/pkg/ecpClient/response_mocks.go similarity index 96% rename from src/ecpClient/response_mocks.go rename to pkg/ecpClient/response_mocks.go index 0f35587..e9787e4 100644 --- a/src/ecpClient/response_mocks.go +++ b/pkg/ecpClient/response_mocks.go @@ -17,7 +17,7 @@ package ecpClient var SuccessResponseMock = `{}` -var AppResponseMock =` +var AppResponseMock = ` test ` @@ -28,7 +28,7 @@ var AppsResponseMock = ` test2 ` -var deviceInfoResponse =` +var deviceInfoResponse = ` test test @@ -96,4 +96,4 @@ var UiResponseMock = ` -` \ No newline at end of file +` diff --git a/pkg/ecpClient/response_structure.go b/pkg/ecpClient/response_structure.go new file mode 100644 index 0000000..7ed80eb --- /dev/null +++ b/pkg/ecpClient/response_structure.go @@ -0,0 +1,135 @@ +/////////////////////////////////////////////////////////////////////////// +// Copyright 2019 Roku, 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 ecpClient + +import ( + "encoding/xml" +) + +type AppsResponse struct { + Apps []App `xml:"app"` +} + +type App struct { + Title string `xml:",chardata"` + ID string `xml:"id,attr"` + Type string `xml:"type,attr"` + Version string `xml:"version,attr"` + Subtype string `xml:"subtype,attr"` +} + +type Player struct { + Error string `xml:"error,attr"` + State string `xml:"state,attr"` + Format struct { + Audio string `xml:"audio,attr"` + Captions string `xml:"captions,attr"` + Container string `xml:"container,attr"` + Drm string `xml:"drm,attr"` + Video string `xml:"video,attr"` + VideoRes string `xml:"video_res,attr"` + } `xml:"format"` + Buffering struct { + Current string `xml:"current,attr"` + Max string `xml:"max,attr"` + Target string `xml:"target,attr"` + } `xml:"buffering"` + NewStream struct { + Speed string `xml:"speed,attr"` + } `xml:"new_stream"` + Position string `xml:"position"` + Duration string `xml:"duration"` + IsLive string `xml:"is_live"` + Runtime string `xml:"runtime"` + StreamSegment struct { + Bitrate string `xml:"bitrate,attr"` + MediaSequence string `xml:"media_sequence,attr"` + SegmentType string `xml:"segment_type,attr"` + Time string `xml:"time,attr"` + } `xml:"stream_segment"` +} + +type ActiveAppResponse struct { + ActiveApp App `xml:"app"` +} + +type Node struct { + XMLName xml.Name + Attrs []xml.Attr `xml:",any,attr"` + Nodes []Node `xml:",any"` +} + +type DeviceInfo struct { + Udn string `xml:"udn"` + SerialNumber string `xml:"serial-number"` + DeviceID string `xml:"device-id"` + AdvertisingID string `xml:"advertising-id"` + VendorName string `xml:"vendor-name"` + ModelName string `xml:"model-name"` + ModelNumber string `xml:"model-number"` + ModelRegion string `xml:"model-region"` + IsTv string `xml:"is-tv"` + IsStick string `xml:"is-stick"` + SupportsEthernet string `xml:"supports-ethernet"` + WifiMac string `xml:"wifi-mac"` + WifiDriver string `xml:"wifi-driver"` + EthernetMac string `xml:"ethernet-mac"` + NetworkType string `xml:"network-type"` + NetworkName string `xml:"network-name"` + FriendlyDeviceName string `xml:"friendly-device-name"` + FriendlyModelName string `xml:"friendly-model-name"` + DefaultDeviceName string `xml:"default-device-name"` + UserDeviceName string `xml:"user-device-name"` + UserDeviceLocation string `xml:"user-device-location"` + BuildNumber string `xml:"build-number"` + SoftwareVersion string `xml:"software-version"` + SoftwareBuild string `xml:"software-build"` + SecureDevice string `xml:"secure-device"` + Language string `xml:"language"` + Country string `xml:"country"` + Locale string `xml:"locale"` + TimeZoneAuto string `xml:"time-zone-auto"` + TimeZone string `xml:"time-zone"` + TimeZoneName string `xml:"time-zone-name"` + TimeZoneTz string `xml:"time-zone-tz"` + TimeZoneOffset string `xml:"time-zone-offset"` + ClockFormat string `xml:"clock-format"` + Uptime string `xml:"uptime"` + PowerMode string `xml:"power-mode"` + SupportsSuspend string `xml:"supports-suspend"` + SupportsFindRemote string `xml:"supports-find-remote"` + FindRemoteIsPossible string `xml:"find-remote-is-possible"` + SupportsAudioGuide string `xml:"supports-audio-guide"` + SupportsRva string `xml:"supports-rva"` + DeveloperEnabled string `xml:"developer-enabled"` + KeyedDeveloperID string `xml:"keyed-developer-id"` + SearchEnabled string `xml:"search-enabled"` + SearchChannelsEnabled string `xml:"search-channels-enabled"` + NotificationsEnabled string `xml:"notifications-enabled"` + NotificationsFirstUse string `xml:"notifications-first-use"` + SupportsPrivateListening string `xml:"supports-private-listening"` + HeadphonesConnected string `xml:"headphones-connected"` + SupportsEcsTextedit string `xml:"supports-ecs-textedit"` + SupportsEcsMicrophone string `xml:"supports-ecs-microphone"` + SupportsWakeOnWlan string `xml:"supports-wake-on-wlan"` + HasPlayOnRoku string `xml:"has-play-on-roku"` + HasMobileScreensaver string `xml:"has-mobile-screensaver"` + SupportURL string `xml:"support-url"` + GrandcentralVersion string `xml:"grandcentral-version"` + TrcVersion string `xml:"trc-version"` + TrcChannelVersion string `xml:"trc-channel-version"` + DavinciVersion string `xml:"davinci-version"` +} diff --git a/pkg/httpServer/handlers.go b/pkg/httpServer/handlers.go new file mode 100644 index 0000000..2547c17 --- /dev/null +++ b/pkg/httpServer/handlers.go @@ -0,0 +1,677 @@ +/////////////////////////////////////////////////////////////////////////// +// Copyright 2019 Roku, 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 httpServer + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "runtime" + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" + ecp "github.com/rokudev/automated-channel-testing/pkg/ecpClient" + "github.com/rokudev/automated-channel-testing/pkg/version" +) + +const defaultPressDelay = 1000 + +type appError struct { + Message string + Code int + InternalCode *int +} + +type appHandler func(http.ResponseWriter, *http.Request) *appError + +func (s *Server) GetStatusHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + arch := runtime.GOARCH + osName := runtime.GOOS + vers := version.BuildVersion + time := version.BuildTime + response := &SessionResponse{ + Status: 0, + Value: &Status{ + Os: OsInfo{ + Arch: arch, + Name: osName, + }, + Build: BuildInfo{ + Version: vers, + Time: time, + }, + }, + } + return prepareResponse(w, response) + } +} + +func (s *Server) GetStartSessionHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + b, err := ioutil.ReadAll(r.Body) + var t Capability + err = json.Unmarshal(b, &t) + if err != nil { + return &appError{err.Error(), http.StatusBadRequest, nil} + } + + if validIP4(t.Ip) != true { + return &appError{"Invalid IP", http.StatusBadRequest, nil} + } + client, err := ecp.GetEcpClient(t.Ip) + if err != nil { + status := responseStatuses["SessionNotCreatedException"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + info, err := client.GetDeviceInfo() + if err != nil { + status := responseStatuses["SessionNotCreatedException"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + id := info.AdvertisingID + session := s.sessions[id] + if session != nil { + status := responseStatuses["SessionNotCreatedException"] + return &appError{"Session already exist", http.StatusInternalServerError, &status} + } + + timeout := t.Timeout + if timeout > 0 { + client.SetTimeout(time.Duration(timeout)) + } + + pressDelay := defaultPressDelay + delay := t.PressDelay + if delay > 0 { + pressDelay = delay + } + + capability := &Capability{ + VendorName: info.VendorName, + ModelName: info.ModelName, + Language: info.Language, + Country: info.Country, + Ip: t.Ip, + PressDelay: pressDelay, + Timeout: client.GetTimeout(), + } + response := &SessionResponse{ + Id: id, + Value: capability, + } + s.sessions[id] = &SessionInfo{ + client, + capability, + time.Duration(pressDelay), + } + + return prepareResponse(w, response) + } +} + +func (s *Server) GetSessionHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + vars := mux.Vars(r) + id := vars["sessionId"] + session := s.sessions[id] + if session == nil { + status := responseStatuses["NoSuchDriver"] + return &appError{"Invalid sessionId", http.StatusInternalServerError, &status} + } + response := &SessionResponse{ + Id: id, + Status: responseStatuses["Success"], + Value: session.capability, + } + return prepareResponse(w, response) + } +} + +func (s *Server) GetSessionsInfoHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + var sessions []*SessionResponse + for key, element := range s.sessions { + response := &SessionResponse{ + Id: key, + Value: element.capability, + } + sessions = append(sessions, response) + } + + return prepareResponse(w, sessions) + } +} + +func (s *Server) GetSessionDeleteHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + vars := mux.Vars(r) + sessionId := vars["sessionId"] + session := s.sessions[sessionId] + if session != nil { + delete(s.sessions, sessionId) + } else { + status := responseStatuses["NoSuchDriver"] + return &appError{"Invalid sessionId", http.StatusInternalServerError, &status} + } + + response := &SessionResponse{ + Id: sessionId, + Status: responseStatuses["Success"], + Value: nil, + } + + return prepareResponse(w, response) + } +} + +func (s *Server) GetTimeoutsHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + client, id, errorInfo := s.getClient(r) + if errorInfo != nil { + return errorInfo + } + b, err := ioutil.ReadAll(r.Body) + var t TimeoutRequest + err = json.Unmarshal(b, &t) + if err != nil { + return &appError{err.Error(), http.StatusBadRequest, nil} + } + timeout := t.Ms + if timeout < 0 { + return &appError{"Timeout must be a positive number.", http.StatusBadRequest, nil} + } + switch t.Type { + case "implicit": + client.SetTimeout(time.Duration(timeout)) + case "pressDelay": + s.sessions[id].pressDelay = time.Duration(timeout) + default: + return &appError{"Invalid \"type\" value.", http.StatusBadRequest, nil} + } + response := &SessionResponse{ + Id: id, + Status: 0, + Value: nil, + } + return prepareResponse(w, response) + } +} + +func (s *Server) GetImplicitTimeoutHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + client, id, errorInfo := s.getClient(r) + if errorInfo != nil { + return errorInfo + } + b, err := ioutil.ReadAll(r.Body) + var t TimeoutRequest + err = json.Unmarshal(b, &t) + if err != nil { + return &appError{err.Error(), http.StatusBadRequest, nil} + } + timeout := t.Ms + if timeout < 0 { + return &appError{"Timeout must be a positive number.", http.StatusBadRequest, nil} + } + urlPath := r.URL.Path + pathParts := strings.Split(urlPath, "/") + timeoutType := pathParts[len(pathParts)-1] + switch timeoutType { + case "implicit_wait": + client.SetTimeout(time.Duration(timeout)) + case "press_wait": + s.sessions[id].pressDelay = time.Duration(timeout) + } + response := &SessionResponse{ + Id: id, + Status: 0, + Value: nil, + } + return prepareResponse(w, response) + } +} + +func (s *Server) GetElementHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + client, id, errorInfo := s.getClient(r) + if errorInfo != nil { + return errorInfo + } + b, err := ioutil.ReadAll(r.Body) + var t ElementRequest + err = json.Unmarshal(b, &t) + if err != nil { + return &appError{err.Error(), http.StatusBadRequest, nil} + } + if t.ElementData == nil { + return &appError{"The \"elementData\" is required ", http.StatusBadRequest, nil} + } + node, err := client.GetAppUi() + if err != nil { + status := responseStatuses["NoSuchDriver"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + result := node.Nodes + var searchError *appError + if t.ParentData != nil { + for _, element := range t.ParentData { + result, searchError = searchMultipleResults(result, element) + if searchError != nil { + return searchError + } + } + } + + for _, element := range t.ElementData { + result, searchError = searchMultipleResults(result, element) + if searchError != nil { + return searchError + } + } + + if result == nil { + status := responseStatuses["NoSuchElement"] + return &appError{"An element could not be located on the screen using the given search parameters", http.StatusInternalServerError, &status} + } + response := &SessionResponse{ + Id: id, + Status: 0, + Value: result[0], + } + + return prepareResponse(w, response) + } +} + +func searchMultipleResults(nodes []ecp.Node, t Element) ([]ecp.Node, *appError) { + var result []ecp.Node + switch t.Using { + case "tag": + result = findMultipleNodes(nodes, t.Value) + case "text": + result = findMultipleNodesByText(nodes, t.Value, "text") + case "attr": + result = findMultipleNodesByText(nodes, t.Value, t.Attribute) + default: + return nil, &appError{"Invalid \"using\" value", http.StatusBadRequest, nil} + } + return result, nil +} + +func (s *Server) GetInstallHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + client, id, errorInfo := s.getClient(r) + if errorInfo != nil { + return errorInfo + } + b, err := ioutil.ReadAll(r.Body) + var t ChannelRequest + err = json.Unmarshal(b, &t) + if err != nil { + return &appError{err.Error(), http.StatusBadRequest, nil} + } + if len(t.ChannelId) == 0 { + return &appError{"The \"channelId\" is required", http.StatusBadRequest, nil} + } + res, err := client.InstallChannel(t.ChannelId) + if err != nil || res == false { + status := responseStatuses["UnknownError"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + response := &SessionResponse{ + Id: id, + Status: responseStatuses["Success"], + Value: nil, + } + return prepareResponse(w, response) + } +} + +func (s *Server) GetLaunchHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + client, id, errorInfo := s.getClient(r) + if errorInfo != nil { + return errorInfo + } + b, err := ioutil.ReadAll(r.Body) + var t ChannelRequest + err = json.Unmarshal(b, &t) + if err != nil { + return &appError{err.Error(), http.StatusBadRequest, nil} + } + if len(t.ChannelId) == 0 { + return &appError{"The \"channelId\" is required", http.StatusBadRequest, nil} + } + res, err := client.LaunchChannel(t.ChannelId, t.ContentId, t.ContentType) + if err != nil || res == false { + status := responseStatuses["UnknownError"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + response := &SessionResponse{ + Id: id, + Status: responseStatuses["Success"], + Value: nil, + } + return prepareResponse(w, response) + } +} + +func (s *Server) GetElementsHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + client, id, errorInfo := s.getClient(r) + if errorInfo != nil { + return errorInfo + } + b, err := ioutil.ReadAll(r.Body) + var t ElementRequest + err = json.Unmarshal(b, &t) + if err != nil { + return &appError{err.Error(), http.StatusBadRequest, nil} + } + if t.ElementData == nil { + return &appError{"The \"elementData\" is required", http.StatusBadRequest, nil} + } + node, err := client.GetAppUi() + if err != nil { + status := responseStatuses["NoSuchDriver"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + result := node.Nodes + var searchError *appError + if t.ParentData != nil { + for _, element := range t.ParentData { + result, searchError = searchMultipleResults(result, element) + if searchError != nil { + return searchError + } + } + } + + for _, element := range t.ElementData { + result, searchError = searchMultipleResults(result, element) + if searchError != nil { + return searchError + } + } + if result == nil { + status := responseStatuses["NoSuchElement"] + return &appError{"An element could not be located on the screen using the given search parameters", http.StatusInternalServerError, &status} + } + response := &SessionResponse{ + Id: id, + Status: 0, + Value: result, + } + + return prepareResponse(w, response) + } +} + +func (s *Server) GetActiveElementHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + client, id, errorInfo := s.getClient(r) + if errorInfo != nil { + return errorInfo + } + node, err := client.GetAppUi() + if err != nil { + status := responseStatuses["NoSuchDriver"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + element := Element{ + Using: "attr", + Value: "true", + Attribute: "focused", + } + result, searchError := searchMultipleResults(node.Nodes, element) + if searchError != nil { + return searchError + } + + if result == nil { + status := responseStatuses["NoSuchElement"] + return &appError{"An element could not be located on the screen using the given search parameters", http.StatusInternalServerError, &status} + } + response := &SessionResponse{ + Id: id, + Status: 0, + Value: result[len(result)-1], + } + return prepareResponse(w, response) + } +} + +func (s *Server) GetAppsHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + client, id, errorInfo := s.getClient(r) + if errorInfo != nil { + return errorInfo + } + apps, err := client.GetApps() + if err != nil { + status := responseStatuses["UnknownError"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + + response := &SessionResponse{ + Status: responseStatuses["Success"], + Id: id, + Value: apps, + } + + return prepareResponse(w, response) + } +} + +func (s *Server) GetPlayerHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + client, id, errorInfo := s.getClient(r) + if errorInfo != nil { + return errorInfo + } + player, err := client.GetPlayer() + if err != nil { + status := responseStatuses["UnknownError"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + + response := &SessionResponse{ + Status: responseStatuses["Success"], + Id: id, + Value: player, + } + + return prepareResponse(w, response) + } +} + +func (s *Server) GetCurrentAppHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + client, id, errorInfo := s.getClient(r) + if errorInfo != nil { + return errorInfo + } + app, err := client.GetActiveApp() + if err != nil { + status := responseStatuses["UnknownError"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + + response := &SessionResponse{ + Status: responseStatuses["Success"], + Id: id, + Value: app, + } + + return prepareResponse(w, response) + } +} + +func (s *Server) GetPressButtonHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + client, id, errorInfo := s.getClient(r) + if errorInfo != nil { + return errorInfo + } + b, err := ioutil.ReadAll(r.Body) + var t ButtonRequest + err = json.Unmarshal(b, &t) + if err != nil { + return &appError{err.Error(), http.StatusBadRequest, nil} + } + + if len([]rune(t.Button)) != 0 { + result, err := client.KeyPress(t.Button) + if err != nil || result == false { + status := responseStatuses["UnknownError"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + } else if len(t.Button_sequence) != 0 { + delays := t.Button_delays + delaysLength := len(delays) + buttons := t.Button_sequence + buttonsLength := len(buttons) + var defaultDelay time.Duration + if delaysLength > 0 { + delayInt, error := strconv.Atoi(delays[delaysLength-1]) + if error != nil { + status := responseStatuses["UnknownError"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + defaultDelay = time.Duration(delayInt) + } else { + defaultDelay = s.sessions[id].pressDelay + } + for i, cmd := range buttons { + result, err := client.KeyPress(cmd) + if err != nil || result == false { + status := responseStatuses["UnknownError"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + if buttonsLength-1 != i { + if delaysLength > i { + delayInt, error := strconv.Atoi(delays[i]) + if error != nil { + status := responseStatuses["UnknownError"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + time.Sleep(time.Duration(delayInt) * time.Millisecond) + } else { + time.Sleep(defaultDelay * time.Millisecond) + } + } + } + } else { + return &appError{"button or button_Sequence is required.", http.StatusBadRequest, nil} + } + response := &SessionResponse{ + Status: responseStatuses["Success"], + Id: id, + Value: nil, + } + return prepareResponse(w, response) + } +} + +func (s *Server) GetSourceHandler() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + client, id, errorInfo := s.getClient(r) + if errorInfo != nil { + return errorInfo + } + result, err := client.GetSource() + if err != nil { + status := responseStatuses["UnknownError"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + response := &SessionResponse{ + Status: responseStatuses["Success"], + Id: id, + Value: result, + } + return prepareResponse(w, response) + } +} + +func (s *Server) notFound() appHandler { + return func(w http.ResponseWriter, r *http.Request) *appError { + return &appError{"Unimplemented Command", http.StatusNotImplemented, nil} + } +} + +//---------------------------Helpers-------------------------------------------- + +func (s *Server) getClient(r *http.Request) (*ecp.EcpClient, string, *appError) { + vars := mux.Vars(r) + id := vars["sessionId"] + session := s.sessions[id] + if session == nil { + status := responseStatuses["NoSuchDriver"] + return nil, id, &appError{"Invalid sessionId", http.StatusInternalServerError, &status} + } + client := session.client + return client, id, nil +} + +func prepareResponse(w http.ResponseWriter, response interface{}) *appError { + js, err := json.Marshal(response) + if err != nil { + status := responseStatuses["UnknownError"] + return &appError{err.Error(), http.StatusInternalServerError, &status} + } + w.Header().Set("Content-Type", "application/json") + w.Write(js) + return nil +} + +func findMultipleNodes(nodes []ecp.Node, value string) []ecp.Node { + var nodeArray []ecp.Node + for _, node := range nodes { + if node.XMLName.Local == value { + nodeArray = append(nodeArray, node) + } + if node.Nodes != nil { + res := findMultipleNodes(node.Nodes, value) + if res != nil { + nodeArray = append(nodeArray, res...) + } + } + } + return nodeArray +} + +func findMultipleNodesByText(nodes []ecp.Node, value string, attribute string) []ecp.Node { + var nodeArray []ecp.Node + for _, node := range nodes { + if node.Attrs != nil { + for _, attr := range node.Attrs { + if attr.Name.Local == attribute && ((attribute == "text" && strings.Contains(attr.Value, value) == true) || (attribute != "text" && attr.Value == value)) { + nodeArray = append(nodeArray, node) + } + } + } + if node.Nodes != nil { + res := findMultipleNodesByText(node.Nodes, value, attribute) + if res != nil { + nodeArray = append(nodeArray, res...) + } + } + } + return nodeArray +} diff --git a/src/httpServer/handlers_test.go b/pkg/httpServer/handlers_test.go similarity index 70% rename from src/httpServer/handlers_test.go rename to pkg/httpServer/handlers_test.go index 3b5eeb6..c96c562 100644 --- a/src/httpServer/handlers_test.go +++ b/pkg/httpServer/handlers_test.go @@ -16,13 +16,14 @@ package httpServer import ( - "github.com/gorilla/mux" - "github.com/stretchr/testify/assert" + "bytes" "net/http" "net/http/httptest" "testing" - ecp "ecpClient" - "bytes" + + "github.com/gorilla/mux" + ecp "github.com/rokudev/automated-channel-testing/pkg/ecpClient" + "github.com/stretchr/testify/assert" ) var sessionId = "test" @@ -33,9 +34,8 @@ func checkError(err error, t *testing.T) { } } - func checkSuccessStatus(code int, t *testing.T) { - if code != http.StatusOK { + if code != http.StatusOK { t.Errorf("Status code differs. Expected %d.\n Got %d", http.StatusOK, code) } } @@ -44,18 +44,18 @@ func GetMockedServerWithSession(res *string) *Server { sessions := make(map[string]*SessionInfo) sessions[sessionId] = &SessionInfo{ capability: &Capability{}, - client: ecp.GetMockedClient(res), + client: ecp.GetMockedClient(res), } server := &Server{ - router: mux.NewRouter(), + router: mux.NewRouter(), sessions: sessions, - } + } return server } func TestAppsHandlerWithValidGet(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, "/session/test/apps", nil) - server := GetMockedServerWithSession(&ecp.AppsResponseMock) + req, err := http.NewRequest(http.MethodGet, "/session/test/apps", nil) + server := GetMockedServerWithSession(&ecp.AppsResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -66,8 +66,8 @@ func TestAppsHandlerWithValidGet(t *testing.T) { } func TestCurrentAppHandlerWithValidGet(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, "/session/test/apps", nil) - server := GetMockedServerWithSession(&ecp.AppResponseMock) + req, err := http.NewRequest(http.MethodGet, "/session/test/apps", nil) + server := GetMockedServerWithSession(&ecp.AppResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -78,8 +78,8 @@ func TestCurrentAppHandlerWithValidGet(t *testing.T) { } func TestPressButtonHandlerWithValidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/session/test/press", bytes.NewBuffer([]byte(`{"button" : "up"}`))) - server := GetMockedServerWithSession(&ecp.SuccessResponseMock) + req, err := http.NewRequest(http.MethodPost, "/session/test/press", bytes.NewBuffer([]byte(`{"button" : "up"}`))) + server := GetMockedServerWithSession(&ecp.SuccessResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -90,8 +90,8 @@ func TestPressButtonHandlerWithValidPost(t *testing.T) { } func TestPressButtonSequenceHandlerWithValidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/session/test/press", bytes.NewBuffer([]byte(`{"button_sequence" : ["up", "right"]}`))) - server := GetMockedServerWithSession(&ecp.SuccessResponseMock) + req, err := http.NewRequest(http.MethodPost, "/session/test/press", bytes.NewBuffer([]byte(`{"button_sequence" : ["up", "right"]}`))) + server := GetMockedServerWithSession(&ecp.SuccessResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -102,8 +102,8 @@ func TestPressButtonSequenceHandlerWithValidPost(t *testing.T) { } func TestInstallHandlerWithValidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/session/test/install", bytes.NewBuffer([]byte(`{"channelId" : "dev"}`))) - server := GetMockedServerWithSession(&ecp.SuccessResponseMock) + req, err := http.NewRequest(http.MethodPost, "/session/test/install", bytes.NewBuffer([]byte(`{"channelId" : "dev"}`))) + server := GetMockedServerWithSession(&ecp.SuccessResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -114,8 +114,8 @@ func TestInstallHandlerWithValidPost(t *testing.T) { } func TestLaunchHandlerWithValidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/session/test/launch", bytes.NewBuffer([]byte(`{"channelId" : "dev"}`))) - server := GetMockedServerWithSession(&ecp.SuccessResponseMock) + req, err := http.NewRequest(http.MethodPost, "/session/test/launch", bytes.NewBuffer([]byte(`{"channelId" : "dev"}`))) + server := GetMockedServerWithSession(&ecp.SuccessResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -126,8 +126,8 @@ func TestLaunchHandlerWithValidPost(t *testing.T) { } func TestDeleteSessionWithValidDelete(t *testing.T) { - req, err := http.NewRequest(http.MethodDelete, "/session/test", nil) - server := GetMockedServerWithSession(&ecp.SuccessResponseMock) + req, err := http.NewRequest(http.MethodDelete, "/session/test", nil) + server := GetMockedServerWithSession(&ecp.SuccessResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -138,8 +138,8 @@ func TestDeleteSessionWithValidDelete(t *testing.T) { } func TestSessionWithValidGet(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, "/session/test", nil) - server := GetMockedServerWithSession(&ecp.SuccessResponseMock) + req, err := http.NewRequest(http.MethodGet, "/session/test", nil) + server := GetMockedServerWithSession(&ecp.SuccessResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -150,8 +150,8 @@ func TestSessionWithValidGet(t *testing.T) { } func TestSessionsInfoWithValidGet(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, "/sessions", nil) - server := GetMockedServerWithSession(&ecp.SuccessResponseMock) + req, err := http.NewRequest(http.MethodGet, "/sessions", nil) + server := GetMockedServerWithSession(&ecp.SuccessResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -162,8 +162,8 @@ func TestSessionsInfoWithValidGet(t *testing.T) { } func TestTimeoutsWithValidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/sessions/test/timeouts", bytes.NewBuffer([]byte(`{"type" : "implicit", "ms": 10}`))) - server := GetMockedServerWithSession(&ecp.SuccessResponseMock) + req, err := http.NewRequest(http.MethodPost, "/sessions/test/timeouts", bytes.NewBuffer([]byte(`{"type" : "implicit", "ms": 10}`))) + server := GetMockedServerWithSession(&ecp.SuccessResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -174,8 +174,8 @@ func TestTimeoutsWithValidPost(t *testing.T) { } func TestTimeoutImplicitWithValidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/sessions/test/timeouts/implicit_wait", bytes.NewBuffer([]byte(`{"ms": 10}`))) - server := GetMockedServerWithSession(&ecp.SuccessResponseMock) + req, err := http.NewRequest(http.MethodPost, "/sessions/test/timeouts/implicit_wait", bytes.NewBuffer([]byte(`{"ms": 10}`))) + server := GetMockedServerWithSession(&ecp.SuccessResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -185,10 +185,9 @@ func TestTimeoutImplicitWithValidPost(t *testing.T) { assert.JSONEq(t, validResponseWithNullValue, rr.Body.String(), "Response body differs") } - func TestTimeoutPressWithValidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/sessions/test/timeouts/press_wait", bytes.NewBuffer([]byte(`{"ms": 10}`))) - server := GetMockedServerWithSession(&ecp.SuccessResponseMock) + req, err := http.NewRequest(http.MethodPost, "/sessions/test/timeouts/press_wait", bytes.NewBuffer([]byte(`{"ms": 10}`))) + server := GetMockedServerWithSession(&ecp.SuccessResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -199,11 +198,11 @@ func TestTimeoutPressWithValidPost(t *testing.T) { } func TestElementWithValidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/session/test/element", bytes.NewBuffer([]byte(`{"elementData": [{ + req, err := http.NewRequest(http.MethodPost, "/session/test/element", bytes.NewBuffer([]byte(`{"elementData": [{ "using": "tag", "value": "Poster" }]}`))) - server := GetMockedServerWithSession(&ecp.UiResponseMock) + server := GetMockedServerWithSession(&ecp.UiResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -214,11 +213,11 @@ func TestElementWithValidPost(t *testing.T) { } func TestElementsWithValidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/session/test/elements", bytes.NewBuffer([]byte(`{ "elementData": [{ + req, err := http.NewRequest(http.MethodPost, "/session/test/elements", bytes.NewBuffer([]byte(`{ "elementData": [{ "using": "tag", "value": "Poster" }]}`))) - server := GetMockedServerWithSession(&ecp.UiResponseMock) + server := GetMockedServerWithSession(&ecp.UiResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -228,10 +227,9 @@ func TestElementsWithValidPost(t *testing.T) { assert.JSONEq(t, validElementsResponse, rr.Body.String(), "Response body differs") } - func TestElementWithInvalidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/session/test/element", bytes.NewBuffer([]byte(`{}`))) - server := GetMockedServerWithSession(&ecp.UiResponseMock) + req, err := http.NewRequest(http.MethodPost, "/session/test/element", bytes.NewBuffer([]byte(`{}`))) + server := GetMockedServerWithSession(&ecp.UiResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -241,8 +239,8 @@ func TestElementWithInvalidPost(t *testing.T) { } func TestElementsWithInvalidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/session/test/elements", bytes.NewBuffer([]byte(`{}`))) - server := GetMockedServerWithSession(&ecp.UiResponseMock) + req, err := http.NewRequest(http.MethodPost, "/session/test/elements", bytes.NewBuffer([]byte(`{}`))) + server := GetMockedServerWithSession(&ecp.UiResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -252,8 +250,8 @@ func TestElementsWithInvalidPost(t *testing.T) { } func TestTimeoutImplicitWithInvalidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/sessions/test/timeouts/implicit_wait", bytes.NewBuffer([]byte(`{"ms": -5}`))) - server := GetMockedServerWithSession(&ecp.SuccessResponseMock) + req, err := http.NewRequest(http.MethodPost, "/sessions/test/timeouts/implicit_wait", bytes.NewBuffer([]byte(`{"ms": -5}`))) + server := GetMockedServerWithSession(&ecp.SuccessResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -263,8 +261,8 @@ func TestTimeoutImplicitWithInvalidPost(t *testing.T) { } func TestInstallHandlerWithInvalidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/session/test/install", bytes.NewBuffer([]byte(`{}`))) - server := GetMockedServerWithSession(&ecp.SuccessResponseMock) + req, err := http.NewRequest(http.MethodPost, "/session/test/install", bytes.NewBuffer([]byte(`{}`))) + server := GetMockedServerWithSession(&ecp.SuccessResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -274,8 +272,8 @@ func TestInstallHandlerWithInvalidPost(t *testing.T) { } func TestLaunchHandlerWithInvalidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/session/test/launch", bytes.NewBuffer([]byte(`{}`))) - server := GetMockedServerWithSession(&ecp.SuccessResponseMock) + req, err := http.NewRequest(http.MethodPost, "/session/test/launch", bytes.NewBuffer([]byte(`{}`))) + server := GetMockedServerWithSession(&ecp.SuccessResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() @@ -285,12 +283,12 @@ func TestLaunchHandlerWithInvalidPost(t *testing.T) { } func TestPressButtonHandlerWithInvalidPost(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, "/session/test/press", bytes.NewBuffer([]byte(`{}`))) - server := GetMockedServerWithSession(&ecp.SuccessResponseMock) + req, err := http.NewRequest(http.MethodPost, "/session/test/press", bytes.NewBuffer([]byte(`{}`))) + server := GetMockedServerWithSession(&ecp.SuccessResponseMock) checkError(err, t) rr := httptest.NewRecorder() r := mux.NewRouter() r.Path("/session/{sessionId}/press").Handler(Middleware(server.GetPressButtonHandler())).Methods("POST") r.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) -} \ No newline at end of file +} diff --git a/src/httpServer/middleware.go b/pkg/httpServer/middleware.go similarity index 100% rename from src/httpServer/middleware.go rename to pkg/httpServer/middleware.go diff --git a/src/httpServer/response_mock.go b/pkg/httpServer/response_mock.go similarity index 100% rename from src/httpServer/response_mock.go rename to pkg/httpServer/response_mock.go diff --git a/src/httpServer/response_structure.go b/pkg/httpServer/response_structure.go similarity index 100% rename from src/httpServer/response_structure.go rename to pkg/httpServer/response_structure.go diff --git a/src/httpServer/routing.go b/pkg/httpServer/routing.go similarity index 100% rename from src/httpServer/routing.go rename to pkg/httpServer/routing.go diff --git a/src/httpServer/server.go b/pkg/httpServer/server.go similarity index 82% rename from src/httpServer/server.go rename to pkg/httpServer/server.go index 47a193c..ab787e6 100644 --- a/src/httpServer/server.go +++ b/pkg/httpServer/server.go @@ -16,32 +16,31 @@ package httpServer import ( - "github.com/gorilla/mux" - ecp "ecpClient" "net/http" "time" + + "github.com/gorilla/mux" + ecp "github.com/rokudev/automated-channel-testing/pkg/ecpClient" "github.com/sirupsen/logrus" ) - - type Server struct { - router *mux.Router + router *mux.Router sessions map[string]*SessionInfo -} +} type SessionInfo struct { - client *ecp.EcpClient + client *ecp.EcpClient capability *Capability pressDelay time.Duration } func GetServerInstance() *Server { server := &Server{ - router: mux.NewRouter(), + router: mux.NewRouter(), sessions: make(map[string]*SessionInfo), } - + return server } @@ -49,8 +48,8 @@ func (s *Server) Start() { s.SetUpRoutes() err := http.ListenAndServe(":9000", nil) if err != http.ErrServerClosed { - logrus.WithError(err).Error("Http Server stopped unexpected") + logrus.WithError(err).Error("Http Server stopped unexpected") } else { - logrus.WithError(err).Info("Http Server stopped") + logrus.WithError(err).Info("Http Server stopped") } - } \ No newline at end of file +} diff --git a/src/httpServer/utils.go b/pkg/httpServer/utils.go similarity index 100% rename from src/httpServer/utils.go rename to pkg/httpServer/utils.go diff --git a/src/version/version.go b/pkg/version/version.go similarity index 100% rename from src/version/version.go rename to pkg/version/version.go diff --git a/src/ecpClient/response_structure.go b/src/ecpClient/response_structure.go deleted file mode 100644 index 9bc38c2..0000000 --- a/src/ecpClient/response_structure.go +++ /dev/null @@ -1,134 +0,0 @@ -/////////////////////////////////////////////////////////////////////////// -// Copyright 2019 Roku, 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 ecpClient - -import ( - "encoding/xml" -) -type AppsResponse struct { - Apps []App `xml:"app"` -} - -type App struct { - Title string `xml:",chardata"` - ID string `xml:"id,attr"` - Type string `xml:"type,attr"` - Version string `xml:"version,attr"` - Subtype string `xml:"subtype,attr"` -} - -type Player struct { - Error string `xml:"error,attr"` - State string `xml:"state,attr"` - Format struct { - Audio string `xml:"audio,attr"` - Captions string `xml:"captions,attr"` - Container string `xml:"container,attr"` - Drm string `xml:"drm,attr"` - Video string `xml:"video,attr"` - VideoRes string `xml:"video_res,attr"` - } `xml:"format"` - Buffering struct { - Current string `xml:"current,attr"` - Max string `xml:"max,attr"` - Target string `xml:"target,attr"` - } `xml:"buffering"` - NewStream struct { - Speed string `xml:"speed,attr"` - } `xml:"new_stream"` - Position string `xml:"position"` - Duration string `xml:"duration"` - IsLive string `xml:"is_live"` - Runtime string `xml:"runtime"` - StreamSegment struct { - Bitrate string `xml:"bitrate,attr"` - MediaSequence string `xml:"media_sequence,attr"` - SegmentType string `xml:"segment_type,attr"` - Time string `xml:"time,attr"` - } `xml:"stream_segment"` -} - -type ActiveAppResponse struct { - ActiveApp App `xml:"app"` -} - -type Node struct { - XMLName xml.Name - Attrs []xml.Attr `xml:",any,attr"` - Nodes []Node `xml:",any"` -} - -type DeviceInfo struct { - Udn string `xml:"udn"` - SerialNumber string `xml:"serial-number"` - DeviceID string `xml:"device-id"` - AdvertisingID string `xml:"advertising-id"` - VendorName string `xml:"vendor-name"` - ModelName string `xml:"model-name"` - ModelNumber string `xml:"model-number"` - ModelRegion string `xml:"model-region"` - IsTv string `xml:"is-tv"` - IsStick string `xml:"is-stick"` - SupportsEthernet string `xml:"supports-ethernet"` - WifiMac string `xml:"wifi-mac"` - WifiDriver string `xml:"wifi-driver"` - EthernetMac string `xml:"ethernet-mac"` - NetworkType string `xml:"network-type"` - NetworkName string `xml:"network-name"` - FriendlyDeviceName string `xml:"friendly-device-name"` - FriendlyModelName string `xml:"friendly-model-name"` - DefaultDeviceName string `xml:"default-device-name"` - UserDeviceName string `xml:"user-device-name"` - UserDeviceLocation string `xml:"user-device-location"` - BuildNumber string `xml:"build-number"` - SoftwareVersion string `xml:"software-version"` - SoftwareBuild string `xml:"software-build"` - SecureDevice string `xml:"secure-device"` - Language string `xml:"language"` - Country string `xml:"country"` - Locale string `xml:"locale"` - TimeZoneAuto string `xml:"time-zone-auto"` - TimeZone string `xml:"time-zone"` - TimeZoneName string `xml:"time-zone-name"` - TimeZoneTz string `xml:"time-zone-tz"` - TimeZoneOffset string `xml:"time-zone-offset"` - ClockFormat string `xml:"clock-format"` - Uptime string `xml:"uptime"` - PowerMode string `xml:"power-mode"` - SupportsSuspend string `xml:"supports-suspend"` - SupportsFindRemote string `xml:"supports-find-remote"` - FindRemoteIsPossible string `xml:"find-remote-is-possible"` - SupportsAudioGuide string `xml:"supports-audio-guide"` - SupportsRva string `xml:"supports-rva"` - DeveloperEnabled string `xml:"developer-enabled"` - KeyedDeveloperID string `xml:"keyed-developer-id"` - SearchEnabled string `xml:"search-enabled"` - SearchChannelsEnabled string `xml:"search-channels-enabled"` - NotificationsEnabled string `xml:"notifications-enabled"` - NotificationsFirstUse string `xml:"notifications-first-use"` - SupportsPrivateListening string `xml:"supports-private-listening"` - HeadphonesConnected string `xml:"headphones-connected"` - SupportsEcsTextedit string `xml:"supports-ecs-textedit"` - SupportsEcsMicrophone string `xml:"supports-ecs-microphone"` - SupportsWakeOnWlan string `xml:"supports-wake-on-wlan"` - HasPlayOnRoku string `xml:"has-play-on-roku"` - HasMobileScreensaver string `xml:"has-mobile-screensaver"` - SupportURL string `xml:"support-url"` - GrandcentralVersion string `xml:"grandcentral-version"` - TrcVersion string `xml:"trc-version"` - TrcChannelVersion string `xml:"trc-channel-version"` - DavinciVersion string `xml:"davinci-version"` -} diff --git a/src/httpServer/handlers.go b/src/httpServer/handlers.go deleted file mode 100644 index b068ede..0000000 --- a/src/httpServer/handlers.go +++ /dev/null @@ -1,676 +0,0 @@ -/////////////////////////////////////////////////////////////////////////// -// Copyright 2019 Roku, 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 httpServer - -import ( - "net/http" - "encoding/json" - ecp "ecpClient" - "io/ioutil" - "github.com/gorilla/mux" - "strings" - "time" - "strconv" - "runtime" - "version" -) - -const defaultPressDelay = 1000 - -type appError struct { - Message string - Code int - InternalCode *int -} - -type appHandler func(http.ResponseWriter, *http.Request) *appError - -func (s *Server) GetStatusHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - arch := runtime.GOARCH - osName := runtime.GOOS - vers := version.BuildVersion - time := version.BuildTime - response := &SessionResponse { - Status: 0, - Value: &Status{ - Os: OsInfo{ - Arch: arch, - Name: osName, - }, - Build: BuildInfo{ - Version: vers, - Time: time, - }, - }, - } - return prepareResponse(w, response) - } - } - -func (s *Server) GetStartSessionHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - b, err := ioutil.ReadAll(r.Body) - var t Capability - err = json.Unmarshal(b, &t) - if err != nil { - return &appError{ err.Error(), http.StatusBadRequest, nil} - } - - if validIP4(t.Ip) != true { - return &appError{ "Invalid IP", http.StatusBadRequest, nil} - } - client, err := ecp.GetEcpClient(t.Ip) - if err != nil { - status := responseStatuses["SessionNotCreatedException"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - info, err := client.GetDeviceInfo() - if err != nil { - status := responseStatuses["SessionNotCreatedException"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - id := info.AdvertisingID - session := s.sessions[id] - if session !=nil { - status := responseStatuses["SessionNotCreatedException"] - return &appError{ "Session already exist", http.StatusInternalServerError, &status} - } - - timeout:= t.Timeout - if timeout > 0 { - client.SetTimeout(time.Duration(timeout)) - } - - pressDelay := defaultPressDelay - delay := t.PressDelay - if delay > 0 { - pressDelay = delay - } - - capability := &Capability { - VendorName: info.VendorName, - ModelName: info.ModelName, - Language: info.Language, - Country: info.Country, - Ip: t.Ip, - PressDelay: pressDelay, - Timeout: client.GetTimeout(), - } - response := &SessionResponse{ - Id: id, - Value: capability, - } - s.sessions[id] = &SessionInfo{ - client, - capability, - time.Duration(pressDelay), - } - - return prepareResponse(w, response) - } - } - -func (s *Server) GetSessionHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - vars := mux.Vars(r) - id := vars["sessionId"] - session := s.sessions[id] - if session == nil { - status := responseStatuses["NoSuchDriver"] - return &appError{ "Invalid sessionId", http.StatusInternalServerError, &status} - } - response := &SessionResponse{ - Id: id, - Status: responseStatuses["Success"], - Value: session.capability, - } - return prepareResponse(w, response) - } - } - -func (s *Server) GetSessionsInfoHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - var sessions []*SessionResponse - for key, element := range s.sessions { - response := &SessionResponse{ - Id: key, - Value: element.capability, - } - sessions = append(sessions, response) - } - - return prepareResponse(w, sessions) - } -} - -func (s *Server) GetSessionDeleteHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - vars := mux.Vars(r) - sessionId := vars["sessionId"] - session := s.sessions[sessionId] - if session != nil { - delete(s.sessions, sessionId) - } else { - status := responseStatuses["NoSuchDriver"] - return &appError{ "Invalid sessionId", http.StatusInternalServerError, &status} - } - - response := &SessionResponse { - Id: sessionId, - Status: responseStatuses["Success"], - Value: nil, - } - - return prepareResponse(w, response) - } - } - -func (s *Server) GetTimeoutsHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - client, id, errorInfo := s.getClient(r) - if errorInfo != nil { - return errorInfo - } - b, err := ioutil.ReadAll(r.Body) - var t TimeoutRequest - err = json.Unmarshal(b, &t) - if err != nil { - return &appError{ err.Error(), http.StatusBadRequest, nil} - } - timeout := t.Ms - if timeout < 0 { - return &appError{ "Timeout must be a positive number.", http.StatusBadRequest, nil} - } - switch t.Type { - case "implicit": - client.SetTimeout(time.Duration(timeout)) - case "pressDelay": - s.sessions[id].pressDelay = time.Duration(timeout) - default: - return &appError{ "Invalid \"type\" value.", http.StatusBadRequest, nil} - } - response := &SessionResponse{ - Id: id, - Status: 0, - Value: nil, - } - return prepareResponse(w, response) - } - } - - func (s *Server) GetImplicitTimeoutHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - client, id, errorInfo := s.getClient(r) - if errorInfo != nil { - return errorInfo - } - b, err := ioutil.ReadAll(r.Body) - var t TimeoutRequest - err = json.Unmarshal(b, &t) - if err != nil { - return &appError{ err.Error(), http.StatusBadRequest, nil} - } - timeout := t.Ms - if timeout < 0 { - return &appError{ "Timeout must be a positive number.", http.StatusBadRequest, nil} - } - urlPath := r.URL.Path - pathParts := strings.Split(urlPath, "/") - timeoutType := pathParts[len(pathParts) - 1] - switch timeoutType { - case "implicit_wait": - client.SetTimeout(time.Duration(timeout)) - case "press_wait": - s.sessions[id].pressDelay = time.Duration(timeout) - } - response := &SessionResponse{ - Id: id, - Status: 0, - Value: nil, - } - return prepareResponse(w, response) - } - } - -func (s *Server) GetElementHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - client, id, errorInfo := s.getClient(r) - if errorInfo != nil { - return errorInfo - } - b, err := ioutil.ReadAll(r.Body) - var t ElementRequest - err = json.Unmarshal(b, &t) - if err != nil { - return &appError{ err.Error(), http.StatusBadRequest, nil} - } - if t.ElementData == nil { - return &appError{ "The \"elementData\" is required ", http.StatusBadRequest, nil} - } - node, err := client.GetAppUi() - if err != nil { - status := responseStatuses["NoSuchDriver"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - result := node.Nodes - var searchError *appError - if t.ParentData != nil { - for _, element := range t.ParentData { - result, searchError = searchMultipleResults(result, element) - if searchError != nil { - return searchError - } - } - } - - for _, element := range t.ElementData { - result, searchError = searchMultipleResults(result, element) - if searchError != nil { - return searchError - } - } - - if result == nil { - status := responseStatuses["NoSuchElement"] - return &appError{ "An element could not be located on the screen using the given search parameters", http.StatusInternalServerError, &status} - } - response := &SessionResponse{ - Id: id, - Status: 0, - Value: result[0], - } - - return prepareResponse(w, response) - } - } - - func searchMultipleResults(nodes []ecp.Node, t Element) ( []ecp.Node, *appError ){ - var result []ecp.Node - switch t.Using { - case "tag": - result = findMultipleNodes(nodes, t.Value) - case "text": - result = findMultipleNodesByText(nodes, t.Value, "text") - case "attr": - result = findMultipleNodesByText(nodes, t.Value, t.Attribute) - default: - return nil, &appError{ "Invalid \"using\" value", http.StatusBadRequest, nil} - } - return result, nil - } - - func (s *Server) GetInstallHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - client, id, errorInfo := s.getClient(r) - if errorInfo != nil { - return errorInfo - } - b, err := ioutil.ReadAll(r.Body) - var t ChannelRequest - err = json.Unmarshal(b, &t) - if err != nil { - return &appError{ err.Error(), http.StatusBadRequest, nil} - } - if len(t.ChannelId) == 0 { - return &appError{ "The \"channelId\" is required", http.StatusBadRequest, nil} - } - res, err := client.InstallChannel(t.ChannelId) - if err !=nil || res == false { - status := responseStatuses["UnknownError"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - response := &SessionResponse { - Id: id, - Status: responseStatuses["Success"], - Value: nil, - } - return prepareResponse(w, response) - } - } - - func (s *Server) GetLaunchHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - client, id, errorInfo := s.getClient(r) - if errorInfo != nil { - return errorInfo - } - b, err := ioutil.ReadAll(r.Body) - var t ChannelRequest - err = json.Unmarshal(b, &t) - if err != nil { - return &appError{ err.Error(), http.StatusBadRequest, nil} - } - if len(t.ChannelId) == 0 { - return &appError{ "The \"channelId\" is required", http.StatusBadRequest, nil} - } - res, err := client.LaunchChannel(t.ChannelId, t.ContentId, t.ContentType) - if err !=nil || res == false { - status := responseStatuses["UnknownError"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - response := &SessionResponse { - Id: id, - Status: responseStatuses["Success"], - Value: nil, - } - return prepareResponse(w, response) - } - } - -func (s *Server) GetElementsHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - client, id, errorInfo := s.getClient(r) - if errorInfo != nil { - return errorInfo - } - b, err := ioutil.ReadAll(r.Body) - var t ElementRequest - err = json.Unmarshal(b, &t) - if err != nil { - return &appError{ err.Error(), http.StatusBadRequest, nil} - } - if t.ElementData == nil { - return &appError{ "The \"elementData\" is required", http.StatusBadRequest, nil} - } - node, err := client.GetAppUi() - if err != nil { - status := responseStatuses["NoSuchDriver"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - result := node.Nodes - var searchError *appError - if t.ParentData != nil { - for _, element := range t.ParentData { - result, searchError = searchMultipleResults(result, element) - if searchError != nil { - return searchError - } - } - } - - for _, element := range t.ElementData { - result, searchError = searchMultipleResults(result, element) - if searchError != nil { - return searchError - } - } - if result == nil { - status := responseStatuses["NoSuchElement"] - return &appError{ "An element could not be located on the screen using the given search parameters", http.StatusInternalServerError, &status} - } - response := &SessionResponse{ - Id: id, - Status: 0, - Value: result, - } - - return prepareResponse(w, response) - } - } - -func (s *Server) GetActiveElementHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - client, id, errorInfo := s.getClient(r) - if errorInfo != nil { - return errorInfo - } - node, err := client.GetAppUi() - if err != nil { - status := responseStatuses["NoSuchDriver"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - element := Element{ - Using: "attr", - Value: "true", - Attribute: "focused", - } - result, searchError := searchMultipleResults(node.Nodes, element) - if searchError != nil { - return searchError - } - - if result == nil { - status := responseStatuses["NoSuchElement"] - return &appError{ "An element could not be located on the screen using the given search parameters", http.StatusInternalServerError, &status} - } - response := &SessionResponse{ - Id: id, - Status: 0, - Value: result[len(result) - 1], - } - return prepareResponse(w, response) - } - } - -func (s *Server) GetAppsHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - client, id, errorInfo := s.getClient(r) - if errorInfo != nil { - return errorInfo - } - apps, err := client.GetApps() - if err != nil { - status := responseStatuses["UnknownError"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - - response := &SessionResponse{ - Status: responseStatuses["Success"], - Id: id, - Value: apps, - } - - return prepareResponse(w, response) - } - } - - func (s *Server) GetPlayerHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - client, id, errorInfo := s.getClient(r) - if errorInfo != nil { - return errorInfo - } - player, err := client.GetPlayer() - if err != nil { - status := responseStatuses["UnknownError"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - - response := &SessionResponse{ - Status: responseStatuses["Success"], - Id: id, - Value: player, - } - - return prepareResponse(w, response) - } - } - -func (s *Server) GetCurrentAppHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - client, id, errorInfo := s.getClient(r) - if errorInfo != nil { - return errorInfo - } - app, err := client.GetActiveApp() - if err != nil { - status := responseStatuses["UnknownError"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - - response := &SessionResponse{ - Status: responseStatuses["Success"], - Id: id, - Value: app, - } - - return prepareResponse(w, response) - } - } - -func (s *Server) GetPressButtonHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - client, id, errorInfo := s.getClient(r) - if errorInfo != nil { - return errorInfo - } - b, err := ioutil.ReadAll(r.Body) - var t ButtonRequest - err = json.Unmarshal(b, &t) - if err != nil { - return &appError{ err.Error(), http.StatusBadRequest, nil} - } - - if len([]rune(t.Button)) != 0 { - result, err := client.KeyPress(t.Button) - if err != nil || result == false { - status := responseStatuses["UnknownError"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - } else if len(t.Button_sequence) != 0 { - delays := t.Button_delays - delaysLength := len(delays) - buttons := t.Button_sequence - buttonsLength := len(buttons) - var defaultDelay time.Duration - if delaysLength > 0 { - delayInt, error := strconv.Atoi(delays[delaysLength - 1]) - if error != nil { - status := responseStatuses["UnknownError"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - defaultDelay = time.Duration(delayInt) - } else { - defaultDelay = s.sessions[id].pressDelay - } - for i, cmd := range buttons { - result, err := client.KeyPress(cmd) - if err != nil || result == false { - status := responseStatuses["UnknownError"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - if buttonsLength - 1 != i { - if delaysLength > i { - delayInt, error := strconv.Atoi(delays[i]) - if error != nil { - status := responseStatuses["UnknownError"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - time.Sleep(time.Duration(delayInt)*time.Millisecond) - } else { - time.Sleep(defaultDelay*time.Millisecond) - } - } - } - } else { - return &appError{ "button or button_Sequence is required.", http.StatusBadRequest, nil} - } - response := &SessionResponse{ - Status: responseStatuses["Success"], - Id: id, - Value: nil, - } - return prepareResponse(w, response) - } - } - -func (s *Server) GetSourceHandler() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - client, id, errorInfo := s.getClient(r) - if errorInfo != nil { - return errorInfo - } - result, err := client.GetSource() - if err != nil { - status := responseStatuses["UnknownError"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - response := &SessionResponse{ - Status: responseStatuses["Success"], - Id: id, - Value: result, - } - return prepareResponse(w, response) - } - } - - func (s *Server) notFound() appHandler { - return func(w http.ResponseWriter, r *http.Request) *appError { - return &appError{"Unimplemented Command", http.StatusNotImplemented, nil} - } - } - - //---------------------------Helpers-------------------------------------------- - - func (s *Server) getClient(r *http.Request) (*ecp.EcpClient, string, *appError) { - vars := mux.Vars(r) - id := vars["sessionId"] - session := s.sessions[id] - if session == nil { - status := responseStatuses["NoSuchDriver"] - return nil, id, &appError{ "Invalid sessionId", http.StatusInternalServerError, &status} - } - client := session.client - return client, id, nil - } - - func prepareResponse(w http.ResponseWriter, response interface{}) *appError { - js, err := json.Marshal(response) - if err != nil { - status := responseStatuses["UnknownError"] - return &appError{ err.Error(), http.StatusInternalServerError, &status} - } - w.Header().Set("Content-Type", "application/json") - w.Write(js) - return nil - } - - func findMultipleNodes(nodes []ecp.Node, value string) []ecp.Node { - var nodeArray []ecp.Node - for _, node := range nodes { - if node.XMLName.Local == value { - nodeArray = append(nodeArray, node) - } - if node.Nodes != nil { - res := findMultipleNodes(node.Nodes, value) - if res != nil { - nodeArray = append(nodeArray, res...) - } - } - } - return nodeArray - } - - func findMultipleNodesByText(nodes []ecp.Node, value string, attribute string) []ecp.Node { - var nodeArray []ecp.Node - for _, node := range nodes { - if node.Attrs != nil { - for _, attr := range node.Attrs { - if attr.Name.Local == attribute && ((attribute == "text" && strings.Contains(attr.Value, value) == true) || (attribute != "text" && attr.Value == value)) { - nodeArray = append(nodeArray, node) - } - } - } - if node.Nodes != nil { - res := findMultipleNodesByText(node.Nodes, value, attribute) - if res != nil { - nodeArray = append(nodeArray, res...) - } - } - } - return nodeArray - } \ No newline at end of file