Skip to content

Commit

Permalink
chore: refactor code and add missing tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mdmfernandes committed Oct 11, 2024
1 parent c586006 commit 442385d
Show file tree
Hide file tree
Showing 12 changed files with 443 additions and 196 deletions.
38 changes: 38 additions & 0 deletions common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package foscam

import (
"io"
"net/http"
)

// Send a GET request to URL via the provided client.
func getRequest(client HTTPClient, url string) ([]byte, error) {
res, err := client.Get(url)
if err != nil {
return nil, &CameraError{err.Error()}
}
defer res.Body.Close()

if res.StatusCode != 200 {
return nil, &BadStatusError{URL: url, Status: res.StatusCode, Expected: http.StatusOK}
}

b, _ := io.ReadAll(res.Body)

return b, nil
}

// Get a snapshot from the URL, via the provided client.
func getSnap(client HTTPClient, url string) ([]byte, error) {
b, err := getRequest(client, url)
if err != nil {
return nil, err
}

// Check that camera returns a JPEG image
if err = isJpeg(b); err != nil {
return nil, err
}

return b, nil
}
155 changes: 155 additions & 0 deletions common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package foscam

import (
"bytes"
"errors"
"io"
"net/http"
"testing"

"github.com/mdmfernandes/go-foscam/mocks"
"github.com/mdmfernandes/go-foscam/testutil"
)

func Test_getRequest(t *testing.T) {
// request() arguments
type args struct {
client *mocks.MockHTTPClient
url string
}

tests := []struct {
name string
args *args
want []byte
wantErr error
wantGetCalled int
}{
{
name: "Camera error",
args: &args{
client: &mocks.MockHTTPClient{
GetFunc: func(url string) (*http.Response, error) {
return nil, errors.New("some error")
},
},
url: "",
},
want: nil,
wantErr: &CameraError{"some error"},
wantGetCalled: 1,
},
{
name: "Unexpected status code (404)",
args: &args{
client: &mocks.MockHTTPClient{
GetFunc: func(url string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusNotFound,
Body: io.NopCloser(bytes.NewBufferString("don't care")),
}, nil
},
},
url: "",
},
want: nil,
wantErr: &BadStatusError{"", http.StatusNotFound, http.StatusOK},
wantGetCalled: 1,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b, err := getRequest(tt.args.client, tt.args.url)

// Check how many times the client was called
if tt.wantGetCalled != tt.args.client.GetCount {
t.Errorf("Number of requests: got = %d; want = %d", tt.args.client.GetCount, tt.wantGetCalled)
}

// Check for errors
if !testutil.EqualError(t, err, tt.wantErr) {
t.Errorf("Error: got = %v; want = %v", err, tt.wantErr)
}

// Check for the response body
if !bytes.Equal(b, tt.want) {
t.Errorf("Response: got = %v; want = %v", b, tt.want)
}
})
}
}

func Test_getSnap(t *testing.T) {
tests := []struct {
name string
client *mocks.MockHTTPClient
url string
want []byte
wantErr error
}{
{
name: "Ok",
client: &mocks.MockHTTPClient{
GetFunc: func(url string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader([]byte("\xFF\xD8\xFF"))),
}, nil
},
},
url: "don't care",
want: []byte("\xFF\xD8\xFF"),
wantErr: nil,
},
{
name: "Request error",
client: &mocks.MockHTTPClient{
GetFunc: func(url string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusForbidden,
Body: io.NopCloser(nil),
}, nil
},
},
url: "",
want: nil,
wantErr: &BadStatusError{URL: "", Status: http.StatusForbidden, Expected: http.StatusOK},
},
{
name: "Not JPEG",
client: &mocks.MockHTTPClient{
GetFunc: func(url string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader([]byte(`this is not JPEG`))),
}, nil
},
},
url: "",
want: nil,
wantErr: &InvalidMIMETypeError{Want: jpegMime, Got: "text/plain; charset=utf-8"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b, err := getSnap(tt.client, tt.url)

// Check how many times the client was called
if tt.client.GetCount != 1 {
t.Errorf("Number of requests: got = %d; want = 1", tt.client.GetCount)
}

// Check for errors
if !testutil.EqualError(t, err, tt.wantErr) {
t.Errorf("Error: got = %v; want = %v", err, tt.wantErr)
}

// Check for the response body
if !bytes.Equal(b, tt.want) {
t.Errorf("Response: got = %v; want = %v", b, tt.want)
}
})
}
}
9 changes: 0 additions & 9 deletions convert.go

This file was deleted.

32 changes: 0 additions & 32 deletions convert_test.go

This file was deleted.

10 changes: 10 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,13 @@ type BadResponseError struct {
func (b *BadResponseError) Error() string {
return fmt.Sprintf("Unexpected response. Want: %#v, Got: %#v", b.Want, b.Got)
}

// BadResponseError represents a bad response from the camera.
type InvalidMIMETypeError struct {
Want any
Got any
}

func (b *InvalidMIMETypeError) Error() string {
return fmt.Sprintf("Invalid MIME type. Want: %#v, Got: %#v", b.Want, b.Got)
}
41 changes: 6 additions & 35 deletions fi8919w.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ package foscam

import (
"fmt"
"io"
"net/http"
"strings"

"github.com/google/go-querystring/query"
)

// fi8919w is a camera Foscam FI8919W
// We don't need to export this struct since we are using an interface fictory
// see the file foscam.go for more details
// We don't need to export this struct since we are using an interface factory.
// See the file foscam.go for more details
type fi8919w struct {
Client HTTPClient `url:"-"`
// The go-querystring values may change from camera to camera, so we can't
Expand All @@ -29,17 +26,11 @@ func (c *fi8919w) ChangeMotionStatus(enable bool) error {
q.Encode(),
b2u(enable))

res, err := c.Client.Get(url)
b, err := getRequest(c.Client, url)
if err != nil {
return &CameraError{err.Error()}
return err
}
defer res.Body.Close()

if res.StatusCode != 200 {
return &BadStatusError{URL: c.URL, Status: res.StatusCode, Expected: http.StatusOK}
}

b, _ := io.ReadAll(res.Body)
got := string(b)
want := "ok.\n"
if got != want {
Expand All @@ -49,30 +40,10 @@ func (c *fi8919w) ChangeMotionStatus(enable bool) error {
return nil
}

// SnapPicture takes a snapshot and returns the picture in a byte slice.
func (c *fi8919w) SnapPicture() ([]byte, error) {
q, _ := query.Values(c)
url := fmt.Sprintf("%s/snapshot.cgi?%s", c.URL, q.Encode())

res, err := c.Client.Get(url)
if err != nil {
return nil, &CameraError{err.Error()}
}
defer res.Body.Close()

if res.StatusCode != 200 {
return nil, &BadStatusError{URL: c.URL, Status: res.StatusCode, Expected: http.StatusOK}
}

b, _ := io.ReadAll(res.Body)

// Check that camera returns a JPEG image
if mime := http.DetectContentType(b); mime != jpegMime {
// If camera returns plain text, show it in the error message
if strings.Contains(mime, "text/plain") {
mime = string(b)
}
return nil, &BadResponseError{Want: jpegMime, Got: mime}
}

return b, nil
return getSnap(c.Client, url)
}
Loading

0 comments on commit 442385d

Please sign in to comment.