1
0
Fork 0
mirror of https://github.com/shouptech/tempgopher.git synced 2026-02-03 16:49:42 +00:00

Compare commits

...

15 commits

18 changed files with 682 additions and 40 deletions

View file

@ -20,9 +20,16 @@ test:
stage: test stage: test
variables: variables:
GIN_MODE: debug GIN_MODE: debug
INFLUXDB_DB: db
INFLUXDB_ADDR: http://influxdb:8086
services:
- influxdb
script: script:
- go get -v -t ./... - go get -v -t ./...
- go test -v - go test -v -coverprofile=$CI_PROJECT_DIR/coverage.out
artifacts:
paths:
- coverage.out
build: build:
stage: build stage: build

View file

@ -1,5 +1,13 @@
# TempGopher Changelog # TempGopher Changelog
## 0.4.0
Release 2018-11-01
* Enable/disable heating or cooling via UI
* Improved unit tests!
* Fixed bug where updating/clearing changes would cause multiple refreshes in a row [#17](https://gitlab.com/shouptech/tempgopher/issues/17)
## 0.3.1 ## 0.3.1
Release: 2018-10-24 Release: 2018-10-24

View file

@ -1,6 +1,6 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found at: https://github.com/gin-gonic/gin/blob/master/LICENSE
// Modified to remove the WWW-Authenticate header for uses in TempGopher // Modified to remove the WWW-Authenticate header for uses in TempGopher

141
auth_test.go Normal file
View file

@ -0,0 +1,141 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found at: https://github.com/gin-gonic/gin/blob/master/LICENSE
// Original source: https://github.com/gin-gonic/gin/blob/master/auth_test.go
// Modified to remove the WWW-Authenticate header for uses in TempGopher
package main
import (
"encoding/base64"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestBasicAuth(t *testing.T) {
pairs := processAccounts(gin.Accounts{
"admin": "password",
"foo": "bar",
"bar": "foo",
})
assert.Len(t, pairs, 3)
assert.Contains(t, pairs, authPair{
user: "bar",
value: "Basic YmFyOmZvbw==",
})
assert.Contains(t, pairs, authPair{
user: "foo",
value: "Basic Zm9vOmJhcg==",
})
assert.Contains(t, pairs, authPair{
user: "admin",
value: "Basic YWRtaW46cGFzc3dvcmQ=",
})
}
func TestBasicAuthFails(t *testing.T) {
assert.Panics(t, func() { processAccounts(nil) })
assert.Panics(t, func() {
processAccounts(gin.Accounts{
"": "password",
"foo": "bar",
})
})
}
func TestBasicAuthSearchCredential(t *testing.T) {
pairs := processAccounts(gin.Accounts{
"admin": "password",
"foo": "bar",
"bar": "foo",
})
user, found := pairs.searchCredential(authorizationHeader("admin", "password"))
assert.Equal(t, "admin", user)
assert.True(t, found)
user, found = pairs.searchCredential(authorizationHeader("foo", "bar"))
assert.Equal(t, "foo", user)
assert.True(t, found)
user, found = pairs.searchCredential(authorizationHeader("bar", "foo"))
assert.Equal(t, "bar", user)
assert.True(t, found)
user, found = pairs.searchCredential(authorizationHeader("admins", "password"))
assert.Empty(t, user)
assert.False(t, found)
user, found = pairs.searchCredential(authorizationHeader("foo", "bar "))
assert.Empty(t, user)
assert.False(t, found)
user, found = pairs.searchCredential("")
assert.Empty(t, user)
assert.False(t, found)
}
func TestBasicAuthAuthorizationHeader(t *testing.T) {
assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password"))
}
func TestBasicAuthSucceed(t *testing.T) {
accounts := gin.Accounts{"admin": "password"}
router := gin.New()
router.Use(BasicAuth(accounts))
router.GET("/login", func(c *gin.Context) {
c.String(http.StatusOK, c.MustGet(gin.AuthUserKey).(string))
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/login", nil)
req.Header.Set("Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "admin", w.Body.String())
}
func TestBasicAuth401(t *testing.T) {
called := false
accounts := gin.Accounts{"foo": "bar"}
router := gin.New()
router.Use(BasicAuth(accounts))
router.GET("/login", func(c *gin.Context) {
called = true
c.String(http.StatusOK, c.MustGet(gin.AuthUserKey).(string))
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/login", nil)
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req)
assert.False(t, called)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestBasicAuth401WithCustomRealm(t *testing.T) {
called := false
accounts := gin.Accounts{"foo": "bar"}
router := gin.New()
router.Use(BasicAuth(accounts))
router.GET("/login", func(c *gin.Context) {
called = true
c.String(http.StatusOK, c.MustGet(gin.AuthUserKey).(string))
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/login", nil)
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req)
assert.False(t, called)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}

7
cli.go
View file

@ -3,6 +3,7 @@ package main
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"io"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@ -40,8 +41,8 @@ func ParsePort(addr string) (uint16, error) {
} }
// PromptForConfiguration walks a user through configuration // PromptForConfiguration walks a user through configuration
func PromptForConfiguration() Config { func PromptForConfiguration(in io.Reader) Config {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(in)
var config Config var config Config
@ -243,7 +244,7 @@ func ConfigCLI(path string) {
os.Exit(1) os.Exit(1)
} }
config := PromptForConfiguration() config := PromptForConfiguration(os.Stdin)
SaveConfig(path, config) SaveConfig(path, config)
} }

39
cli_test.go Normal file
View file

@ -0,0 +1,39 @@
package main
import (
"bufio"
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_ReadInput(t *testing.T) {
// Test basic input
buf := bytes.NewBufferString("foobar\n")
reader := bufio.NewReader(buf)
resp := ReadInput(reader, "")
assert.Equal(t, "foobar", resp)
// Test default input
buf = bytes.NewBufferString("\n")
reader = bufio.NewReader(buf)
resp = ReadInput(reader, "default")
assert.Equal(t, "default", resp)
// Test that a panic occurred
buf = bytes.NewBufferString("")
reader = bufio.NewReader(buf)
assert.Panics(t, func() { ReadInput(reader, "") })
}
func Test_ParsePort(t *testing.T) {
// Test that 600 is parsed succesfully
port, err := ParsePort(":600")
assert.Equal(t, uint16(600), port)
assert.Equal(t, nil, err)
// Test failure
port, err = ParsePort("")
assert.NotEqual(t, nil, err)
}

View file

@ -3,10 +3,10 @@ package main
import ( import (
"errors" "errors"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"syscall" "syscall"
"github.com/jinzhu/copier"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -65,17 +65,7 @@ func UpdateSensorConfig(s Sensor) error {
for i := range config.Sensors { for i := range config.Sensors {
if config.Sensors[i].ID == s.ID { if config.Sensors[i].ID == s.ID {
config.Sensors[i].Alias = s.Alias copier.Copy(&config.Sensors[i], &s)
config.Sensors[i].HighTemp = s.HighTemp
config.Sensors[i].LowTemp = s.LowTemp
config.Sensors[i].HeatGPIO = s.HeatGPIO
config.Sensors[i].HeatInvert = s.HeatInvert
config.Sensors[i].HeatMinutes = s.HeatMinutes
config.Sensors[i].CoolGPIO = s.CoolGPIO
config.Sensors[i].CoolInvert = s.CoolInvert
config.Sensors[i].CoolMinutes = s.CoolMinutes
config.Sensors[i].Verbose = s.Verbose
log.Println(config.Sensors[i])
} }
} }
@ -83,8 +73,6 @@ func UpdateSensorConfig(s Sensor) error {
return err return err
} }
log.Println(config.Sensors[0])
if err = SignalReload(); err != nil { if err = SignalReload(); err != nil {
return err return err
} }

View file

@ -1,11 +1,99 @@
package main package main
import ( import (
"io/ioutil"
"os"
"os/signal"
"syscall"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func Test_UpdateSensorConfig(t *testing.T) {
testConfig := Config{
Sensors: []Sensor{
Sensor{
Alias: "foo",
CoolDisable: true,
},
},
Users: []User{},
ListenAddr: ":8080",
}
newSensor := Sensor{Alias: "bar", CoolDisable: false}
// Create a temp file
tmpfile, err := ioutil.TempFile("", "tempgopher")
assert.Equal(t, nil, err)
defer os.Remove(tmpfile.Name()) // Remove the tempfile when done
configFilePath = tmpfile.Name()
// Save to tempfile
err = SaveConfig(tmpfile.Name(), testConfig)
assert.Equal(t, nil, err)
// Create a channel to capture SIGHUP
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGHUP)
// Update the stored config
UpdateSensorConfig(newSensor)
// Load the config
config, err := LoadConfig(tmpfile.Name())
assert.Equal(t, nil, err)
assert.Equal(t, "bar", config.Sensors[0].Alias)
// Validate SIGHUP
ret := <-sig
assert.Equal(t, syscall.SIGHUP, ret)
}
func Test_SignalReload(t *testing.T) {
// Create a channel to capture SIGHUP
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGHUP)
// Generate SIGHUP
err := SignalReload()
assert.Equal(t, nil, err)
// Validate SIGHUP
ret := <-sig
assert.Equal(t, syscall.SIGHUP, ret)
}
func Test_SaveConfig(t *testing.T) {
// Save zero-valued config
testConfig := Config{
Sensors: []Sensor{},
Users: []User{},
ListenAddr: ":8080",
}
// Test writing to a path that doesn't exist
err := SaveConfig("/this/does/not/exist", testConfig)
assert.NotEqual(t, nil, err)
// Create a temp file
tmpfile, err := ioutil.TempFile("", "tempgopher")
assert.Equal(t, nil, err)
defer os.Remove(tmpfile.Name()) // Remove the tempfile when done
// Save to tempfile
err = SaveConfig(tmpfile.Name(), testConfig)
assert.Equal(t, nil, err)
// Load the config
config, err := LoadConfig(tmpfile.Name())
assert.Equal(t, nil, err)
assert.Equal(t, testConfig, *config)
}
func Test_LoadConfig(t *testing.T) { func Test_LoadConfig(t *testing.T) {
testConfig := Config{ testConfig := Config{
Sensors: []Sensor{ Sensors: []Sensor{
@ -30,17 +118,23 @@ func Test_LoadConfig(t *testing.T) {
}, },
}, },
BaseURL: "https://foo.bar", BaseURL: "https://foo.bar",
ListenAddr: "127.0.0.1:8080", ListenAddr: ":8080",
DisplayFahrenheit: true, DisplayFahrenheit: true,
Influx: Influx{Addr: "http://foo:8086"}, Influx: Influx{Addr: "http://foo:8086"},
} }
// Test loading of config
loadedConfig, err := LoadConfig("tests/test_config.yml") loadedConfig, err := LoadConfig("tests/test_config.yml")
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.Equal(t, &testConfig, loadedConfig) assert.Equal(t, &testConfig, loadedConfig)
// Test for failures with duplicate IDs and Aliases
_, err = LoadConfig("tests/duplicate_id.yml") _, err = LoadConfig("tests/duplicate_id.yml")
assert.NotEqual(t, nil, err) assert.NotEqual(t, nil, err)
_, err = LoadConfig("tests/duplicate_alias.yml") _, err = LoadConfig("tests/duplicate_alias.yml")
assert.NotEqual(t, nil, err) assert.NotEqual(t, nil, err)
// Test for non-existence
_, err = LoadConfig("DNE")
assert.NotEqual(t, nil, err)
} }

7
html/css/custom.css Normal file
View file

@ -0,0 +1,7 @@
label {
font-weight: normal;
}
input[type="checkbox"] {
margin-right: 0.5em;
}

View file

@ -7,6 +7,7 @@
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css"> <link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="css/normalize.css"> <link rel="stylesheet" href="css/normalize.css">
<link rel="stylesheet" href="css/skeleton.css"> <link rel="stylesheet" href="css/skeleton.css">
<link rel="stylesheet" href="css/custom.css">
<link rel="icon" type="image/png" href="img/favicon.png"> <link rel="icon" type="image/png" href="img/favicon.png">
<script src="js/jquery.min.js"></script> <script src="js/jquery.min.js"></script>
<script src="/jsconfig.js"></script> <script src="/jsconfig.js"></script>

View file

@ -69,6 +69,7 @@ function appendData(data) {
var rowdiv = $("<div></div>"); var rowdiv = $("<div></div>");
rowdiv.addClass("row"); rowdiv.addClass("row");
////////////////////////////////////////////////////////////////////////////
// Display temperature // Display temperature
if (jsconfig.fahrenheit) { if (jsconfig.fahrenheit) {
var temp = celsiusToFahrenheit(parseFloat(data.temp)).toFixed(1) + "°F"; var temp = celsiusToFahrenheit(parseFloat(data.temp)).toFixed(1) + "°F";
@ -79,6 +80,7 @@ function appendData(data) {
var tempdiv = $("<div></div>").addClass("two columns").append(temph); var tempdiv = $("<div></div>").addClass("two columns").append(temph);
rowdiv.append(tempdiv); rowdiv.append(tempdiv);
////////////////////////////////////////////////////////////////////////////
// Display status // Display status
if (data.cooling) { if (data.cooling) {
var statustext = "Cooling" var statustext = "Cooling"
@ -88,14 +90,16 @@ function appendData(data) {
var statustext = "Idle" var statustext = "Idle"
} }
var statusp = $("<p></p>").html(statustext); var statusp = $("<p></p>").html(statustext);
var statusdiv = $("<div></div>").addClass("two columns").append(statusp); var statusdiv = $("<div></div>").addClass("one columns").append(statusp);
rowdiv.append(statusdiv); rowdiv.append(statusdiv);
// Display sensor config // Make AJAX call to get current configuration of the sensor
$.ajax({ $.ajax({
url: jsconfig.baseurl + "/api/config/sensors/" + data.alias, url: jsconfig.baseurl + "/api/config/sensors/" + data.alias,
beforeSend: authHeaders beforeSend: authHeaders
}).then(function(configData){ }).then(function(configData){
////////////////////////////////////////////////////////////////////////
// Display current configuration
if (jsconfig.fahrenheit) { if (jsconfig.fahrenheit) {
var degUnit = "°F"; var degUnit = "°F";
var hightemp = celsiusToFahrenheit(parseFloat(configData.hightemp)).toFixed(1); var hightemp = celsiusToFahrenheit(parseFloat(configData.hightemp)).toFixed(1);
@ -125,10 +129,32 @@ function appendData(data) {
configp.append("Heats for ").append(hmIn).append(" minutes when &lt; ").append(ltIn).append(degUnit); configp.append("Heats for ").append(hmIn).append(" minutes when &lt; ").append(ltIn).append(degUnit);
} }
var configdiv = $("<div></div>").addClass("five columns").append(configp); var configdiv = $("<div></div>").addClass("six columns").append(configp);
rowdiv.append(configdiv); rowdiv.append(configdiv);
var yesButton = $("<button></button>").addClass("button button-primary").text("✔").css("margin-right", "5px").click(function() { ////////////////////////////////////////////////////////////////////////
// Display options to turn heating/cooling on/off
var conChecked = false;
if (!configData.cooldisable) {
conChecked = true;
}
var con = $('<input type="checkbox">').attr("id", "con" + configData.alias).prop('checked', conChecked).on('input', function(){window.clearInterval(rtHandle)})
var coolCheck = $('<label></label>').text("❄️").prepend(con);
var honChecked = false;
if (!configData.heatdisable) {
honChecked = true;
}
var hon = $('<input type="checkbox">').attr("id", "hon" + configData.alias).prop('checked', honChecked).on('input', function(){window.clearInterval(rtHandle)})
var heatCheck = $('<label></label>').text("♨").prepend(hon);
var offOnDiv = $("<div></div>").addClass("one columns").append(coolCheck).append(heatCheck);
rowdiv.append(offOnDiv);
////////////////////////////////////////////////////////////////////////
// Create yes and no buttons
var yesButton = $("<button></button>").addClass("button button-primary").text("✔").click(function() {
if (jsconfig.fahrenheit) { if (jsconfig.fahrenheit) {
var newHT = fahrenheitToCelsius(parseFloat(htIn.val())); var newHT = fahrenheitToCelsius(parseFloat(htIn.val()));
var newLT = fahrenheitToCelsius(parseFloat(ltIn.val())); var newLT = fahrenheitToCelsius(parseFloat(ltIn.val()));
@ -148,25 +174,27 @@ function appendData(data) {
"heatgpio": configData.heatgpio, "heatgpio": configData.heatgpio,
"heatinvert": configData.heatInvert, "heatinvert": configData.heatInvert,
"heatminutes": parseFloat(hmIn.val()), "heatminutes": parseFloat(hmIn.val()),
"heatdisable": !hon.is(":checked"),
"coolgpio": configData.coolgpio, "coolgpio": configData.coolgpio,
"coolinvert": configData.coolinvert, "coolinvert": configData.coolinvert,
"coolminutes": parseFloat(cmIn.val()), "coolminutes": parseFloat(cmIn.val()),
"cooldisable": !con.is(":checked"),
"verbose": configData.verbose "verbose": configData.verbose
}]) }])
}) });
window.setInterval(renderThermostats, 60000); window.clearInterval(rtHandle);
rtHandle = window.setInterval(renderThermostats, 60000);
renderThermostats(); renderThermostats();
}); });
var noButton = $("<button></button>").addClass("button").text("✘").click(function() { var noButton = $("<button></button>").addClass("button").text("✘").click(function() {
window.setInterval(renderThermostats, 60000); window.clearInterval(rtHandle);
rtHandle = window.setInterval(renderThermostats, 60000);
renderThermostats(); renderThermostats();
}); });
if (!configData.heatdisable || !configData.cooldisable) { var buttonDiv = $("<div></div>").addClass("two columns").append(yesButton).append($("<br>")).append(noButton);
var buttonDiv = $("<div></div>").addClass("three columns").append(yesButton).append(noButton);
rowdiv.append(buttonDiv); rowdiv.append(buttonDiv);
}
// Add things back to the thermostat list // Add things back to the thermostat list
$("#thermostats").append(titlediv); $("#thermostats").append(titlediv);
@ -180,8 +208,16 @@ function renderThermostats() {
beforeSend: authHeaders beforeSend: authHeaders
}).then(function(data) { }).then(function(data) {
$("#thermostats").empty(); $("#thermostats").empty();
// Sort by sensor alias
var sorted = [];
for(var key in data) { for(var key in data) {
appendData(data[key]) sorted[sorted.length] = key;
}
sorted.sort();
for (var i in sorted) {
appendData(data[sorted[i]])
}; };
}); });
}; };

26
influx_test.go Normal file
View file

@ -0,0 +1,26 @@
package main
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_WriteStateToInflux(t *testing.T) {
influxAddr := os.Getenv("INFLUXDB_ADDR")
state := State{Temp: 32}
// Test failure with empty config
err := WriteStateToInflux(state, Influx{})
assert.NotEqual(t, nil, err)
// Test failure with missing database
err = WriteStateToInflux(state, Influx{Addr: influxAddr})
assert.NotEqual(t, nil, err)
// Test success with writing to database
config := Influx{Addr: influxAddr, Database: "db"}
err = WriteStateToInflux(state, config)
assert.Equal(t, nil, err)
}

View file

@ -8,7 +8,7 @@ import (
) )
// Version is the current code version of tempgopher // Version is the current code version of tempgopher
const Version = "0.3.1" const Version = "0.4.0"
func main() { func main() {
var args struct { var args struct {

View file

@ -24,5 +24,4 @@ sensors:
users: users:
- foo: bar - foo: bar
baseurl: https://foo.bar baseurl: https://foo.bar
listenaddr: 127.0.0.1:8080
displayfahrenheit: true displayfahrenheit: true

View file

@ -24,5 +24,4 @@ sensors:
users: users:
- foo: bar - foo: bar
baseurl: https://foo.bar baseurl: https://foo.bar
listenaddr: 127.0.0.1:8080
displayfahrenheit: true displayfahrenheit: true

View file

@ -14,7 +14,6 @@ users:
- name: foo - name: foo
password: bar password: bar
baseurl: https://foo.bar baseurl: https://foo.bar
listenaddr: 127.0.0.1:8080
displayfahrenheit: true displayfahrenheit: true
influx: influx:
addr: http://foo:8086 addr: http://foo:8086

13
web.go
View file

@ -104,6 +104,15 @@ func VersionHandler(c *gin.Context) {
c.JSON(http.StatusOK, version{Version: Version}) c.JSON(http.StatusOK, version{Version: Version})
} }
// AppHandler returns 301 to /app
func AppHandler(config *Config) gin.HandlerFunc {
fn := func(c *gin.Context) {
c.Redirect(http.StatusPermanentRedirect, config.BaseURL+"/app/")
}
return fn
}
// SetupRouter initializes the gin router. // SetupRouter initializes the gin router.
func SetupRouter(config *Config, states *map[string]State) *gin.Engine { func SetupRouter(config *Config, states *map[string]State) *gin.Engine {
// If not specified, put gin in release mode // If not specified, put gin in release mode
@ -146,9 +155,7 @@ func SetupRouter(config *Config, states *map[string]State) *gin.Engine {
r.StaticFS("/app", GetBox()) r.StaticFS("/app", GetBox())
// Redirect / to /app // Redirect / to /app
r.Any("/", func(c *gin.Context) { r.Any("/", AppHandler(config))
c.Redirect(301, config.BaseURL+"/app/")
})
return r return r
} }

290
web_test.go Normal file
View file

@ -0,0 +1,290 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"os/signal"
"strconv"
"syscall"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func Test_PingHandler(t *testing.T) {
r := gin.New()
r.GET("/ping", PingHandler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/ping", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "pong", w.Body.String())
}
func Test_ConfigHandler(t *testing.T) {
testConfig := Config{
Sensors: []Sensor{
Sensor{
Alias: "foo",
},
},
Users: []User{},
ListenAddr: ":8080",
}
r := gin.New()
r.GET("/config", ConfigHandler(&testConfig))
r.GET("/config/sensors/*alias", ConfigHandler(&testConfig))
// Validate GET request /config
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/config", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
jc, _ := json.Marshal(testConfig)
assert.Equal(t, string(jc), w.Body.String())
// Validate GET request to /config/sensors
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/config/sensors/", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
jc, _ = json.Marshal(testConfig.Sensors)
assert.Equal(t, string(jc), w.Body.String())
// Validate GET request /config/sensors/foo
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/config/sensors/foo", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
jc, _ = json.Marshal(testConfig.Sensors[0])
assert.Equal(t, string(jc), w.Body.String())
// Validate not ofund
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/config/sensors/DNE", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func Test_UpdateSensorsHandler(t *testing.T) {
r := gin.New()
r.POST("/config/sensors", UpdateSensorsHandler)
// Test bad request
buf := bytes.NewBufferString("foobar")
reader := bufio.NewReader(buf)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/config/sensors", reader)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
// Test good request
testConfig := Config{
Sensors: []Sensor{
Sensor{
Alias: "foo",
},
},
Users: []User{},
ListenAddr: ":8080",
}
newSensor := []Sensor{Sensor{Alias: "bar"}}
// Create a temp file
tmpfile, err := ioutil.TempFile("", "tempgopher")
assert.Equal(t, nil, err)
defer os.Remove(tmpfile.Name()) // Remove the tempfile when done
configFilePath = tmpfile.Name()
// Save to tempfile
err = SaveConfig(tmpfile.Name(), testConfig)
assert.Equal(t, nil, err)
// Create a channel to capture SIGHUP
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGHUP)
// Test a POST call
j, _ := json.Marshal(newSensor)
buf = bytes.NewBufferString(string(j))
reader = bufio.NewReader(buf)
w = httptest.NewRecorder()
req, _ = http.NewRequest("POST", "/config/sensors", reader)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Test internal server error
configFilePath = "/this/does/not/exist"
j, _ = json.Marshal(newSensor)
buf = bytes.NewBufferString(string(j))
reader = bufio.NewReader(buf)
w = httptest.NewRecorder()
req, _ = http.NewRequest("POST", "/config/sensors", reader)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
}
func Test_StatusHandler(t *testing.T) {
states := make(map[string]State)
states["foo"] = State{Temp: 5}
r := gin.New()
r.GET("/status", StatusHandler(&states))
r.GET("/status/*alias", StatusHandler(&states))
// Test all states retrieval
j, _ := (json.Marshal(states))
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/status", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, string(j), w.Body.String())
// Test specific state
j, _ = (json.Marshal(states["foo"]))
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/status/foo", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, string(j), w.Body.String())
// Test not found
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/status/DNE", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func Test_JSConfigHandler(t *testing.T) {
testConfig := Config{
BaseURL: "http://localhost:8080",
DisplayFahrenheit: true,
}
jsconfig := "var jsconfig={baseurl:\"" + testConfig.BaseURL +
"\",fahrenheit:" + strconv.FormatBool(testConfig.DisplayFahrenheit) + "};"
r := gin.New()
r.GET("/jsconfig.js", JSConfigHandler(&testConfig))
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/jsconfig.js", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, jsconfig, w.Body.String())
}
func Test_VersionHandler(t *testing.T) {
type version struct {
Version string `json:"version"`
}
j, _ := json.Marshal(version{Version: Version})
r := gin.New()
r.GET("/version", VersionHandler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/version", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, string(j), w.Body.String())
}
func Test_AppHandler(t *testing.T) {
testConfig := Config{BaseURL: "http://localhost:8080"}
location := testConfig.BaseURL + "/app/"
r := gin.New()
r.Any("/", AppHandler(&testConfig))
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusPermanentRedirect, w.Code)
assert.Equal(t, location, w.Header().Get("Location"))
}
func Test_SetupRouter(t *testing.T) {
testConfig := Config{
Sensors: []Sensor{
Sensor{
Alias: "foo",
},
},
Users: []User{},
ListenAddr: ":8080",
BaseURL: "http://localhost:8080",
}
states := make(map[string]State)
states["foo"] = State{}
// Create a temp file
tmpfile, err := ioutil.TempFile("", "tempgopher")
assert.Equal(t, nil, err)
defer os.Remove(tmpfile.Name()) // Remove the tempfile when done
configFilePath = tmpfile.Name()
// Setup a router
r := SetupRouter(&testConfig, &states)
assert.IsType(t, gin.New(), r)
}
func Test_GetGinAccounts(t *testing.T) {
testConfig := Config{
Users: []User{
User{
Name: "mike",
Password: "12345",
},
},
}
testUsers := make(gin.Accounts)
testUsers["mike"] = "12345"
actualUsers := GetGinAccounts(&testConfig)
assert.Equal(t, testUsers, actualUsers)
}
func Test_reloadWebConfig(t *testing.T) {
// Save zero-valued config
testConfig := Config{
Sensors: []Sensor{},
Users: []User{},
ListenAddr: ":8080",
}
newConfig := Config{
Sensors: []Sensor{},
Users: []User{},
BaseURL: "http://localhost:8080",
ListenAddr: ":8080",
}
// Create a temp file to store newConfig
tmpfile, err := ioutil.TempFile("", "tempgopher")
assert.Equal(t, nil, err)
defer os.Remove(tmpfile.Name()) // Remove the tempfile when done
SaveConfig(tmpfile.Name(), newConfig)
// Test that newConfig is copied to testConfig
err = reloadWebConfig(&testConfig, tmpfile.Name())
assert.Equal(t, nil, err)
assert.Equal(t, testConfig, newConfig)
// Test the error case
err = reloadWebConfig(&testConfig, "/does/not/exist")
assert.NotEqual(t, nil, err)
}