mirror of
https://github.com/shouptech/tempgopher.git
synced 2026-02-03 16:49:42 +00:00
Compare commits
15 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 209c3afc2a | |||
| a6196d5fa8 | |||
| 8aebae0ebe | |||
| 924473090e | |||
| 927f5b9043 | |||
| 1b946b9ce8 | |||
| 14fa9a78c4 | |||
| 961a8de916 | |||
| 7ad2619369 | |||
| 14b5cc4a8b | |||
| 281af2b255 | |||
| 32f1c7fc9d | |||
| 1ce594d540 | |||
| a167da2230 | |||
| 195d167664 |
18 changed files with 682 additions and 40 deletions
|
|
@ -20,9 +20,16 @@ test:
|
|||
stage: test
|
||||
variables:
|
||||
GIN_MODE: debug
|
||||
INFLUXDB_DB: db
|
||||
INFLUXDB_ADDR: http://influxdb:8086
|
||||
services:
|
||||
- influxdb
|
||||
script:
|
||||
- go get -v -t ./...
|
||||
- go test -v
|
||||
- go test -v -coverprofile=$CI_PROJECT_DIR/coverage.out
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage.out
|
||||
|
||||
build:
|
||||
stage: build
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
# 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
|
||||
|
||||
Release: 2018-10-24
|
||||
|
|
|
|||
2
auth.go
2
auth.go
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||
// 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
|
||||
|
||||
|
|
|
|||
141
auth_test.go
Normal file
141
auth_test.go
Normal 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
7
cli.go
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -40,8 +41,8 @@ func ParsePort(addr string) (uint16, error) {
|
|||
}
|
||||
|
||||
// PromptForConfiguration walks a user through configuration
|
||||
func PromptForConfiguration() Config {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
func PromptForConfiguration(in io.Reader) Config {
|
||||
reader := bufio.NewReader(in)
|
||||
|
||||
var config Config
|
||||
|
||||
|
|
@ -243,7 +244,7 @@ func ConfigCLI(path string) {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
config := PromptForConfiguration()
|
||||
config := PromptForConfiguration(os.Stdin)
|
||||
|
||||
SaveConfig(path, config)
|
||||
}
|
||||
|
|
|
|||
39
cli_test.go
Normal file
39
cli_test.go
Normal 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)
|
||||
}
|
||||
16
config.go
16
config.go
|
|
@ -3,10 +3,10 @@ package main
|
|||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/jinzhu/copier"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
|
|
@ -65,17 +65,7 @@ func UpdateSensorConfig(s Sensor) error {
|
|||
|
||||
for i := range config.Sensors {
|
||||
if config.Sensors[i].ID == s.ID {
|
||||
config.Sensors[i].Alias = s.Alias
|
||||
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])
|
||||
copier.Copy(&config.Sensors[i], &s)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,8 +73,6 @@ func UpdateSensorConfig(s Sensor) error {
|
|||
return err
|
||||
}
|
||||
|
||||
log.Println(config.Sensors[0])
|
||||
|
||||
if err = SignalReload(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,99 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"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) {
|
||||
testConfig := Config{
|
||||
Sensors: []Sensor{
|
||||
|
|
@ -30,17 +118,23 @@ func Test_LoadConfig(t *testing.T) {
|
|||
},
|
||||
},
|
||||
BaseURL: "https://foo.bar",
|
||||
ListenAddr: "127.0.0.1:8080",
|
||||
ListenAddr: ":8080",
|
||||
DisplayFahrenheit: true,
|
||||
Influx: Influx{Addr: "http://foo:8086"},
|
||||
}
|
||||
|
||||
// Test loading of config
|
||||
loadedConfig, err := LoadConfig("tests/test_config.yml")
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, &testConfig, loadedConfig)
|
||||
|
||||
// Test for failures with duplicate IDs and Aliases
|
||||
_, err = LoadConfig("tests/duplicate_id.yml")
|
||||
assert.NotEqual(t, nil, err)
|
||||
_, err = LoadConfig("tests/duplicate_alias.yml")
|
||||
assert.NotEqual(t, nil, err)
|
||||
|
||||
// Test for non-existence
|
||||
_, err = LoadConfig("DNE")
|
||||
assert.NotEqual(t, nil, err)
|
||||
}
|
||||
|
|
|
|||
7
html/css/custom.css
Normal file
7
html/css/custom.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
label {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
<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/skeleton.css">
|
||||
<link rel="stylesheet" href="css/custom.css">
|
||||
<link rel="icon" type="image/png" href="img/favicon.png">
|
||||
<script src="js/jquery.min.js"></script>
|
||||
<script src="/jsconfig.js"></script>
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ function appendData(data) {
|
|||
var rowdiv = $("<div></div>");
|
||||
rowdiv.addClass("row");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Display temperature
|
||||
if (jsconfig.fahrenheit) {
|
||||
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);
|
||||
rowdiv.append(tempdiv);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Display status
|
||||
if (data.cooling) {
|
||||
var statustext = "Cooling"
|
||||
|
|
@ -88,14 +90,16 @@ function appendData(data) {
|
|||
var statustext = "Idle"
|
||||
}
|
||||
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);
|
||||
|
||||
// Display sensor config
|
||||
// Make AJAX call to get current configuration of the sensor
|
||||
$.ajax({
|
||||
url: jsconfig.baseurl + "/api/config/sensors/" + data.alias,
|
||||
beforeSend: authHeaders
|
||||
}).then(function(configData){
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Display current configuration
|
||||
if (jsconfig.fahrenheit) {
|
||||
var degUnit = "°F";
|
||||
var hightemp = celsiusToFahrenheit(parseFloat(configData.hightemp)).toFixed(1);
|
||||
|
|
@ -125,10 +129,32 @@ function appendData(data) {
|
|||
configp.append("Heats for ").append(hmIn).append(" minutes when < ").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);
|
||||
|
||||
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) {
|
||||
var newHT = fahrenheitToCelsius(parseFloat(htIn.val()));
|
||||
var newLT = fahrenheitToCelsius(parseFloat(ltIn.val()));
|
||||
|
|
@ -148,25 +174,27 @@ function appendData(data) {
|
|||
"heatgpio": configData.heatgpio,
|
||||
"heatinvert": configData.heatInvert,
|
||||
"heatminutes": parseFloat(hmIn.val()),
|
||||
"heatdisable": !hon.is(":checked"),
|
||||
"coolgpio": configData.coolgpio,
|
||||
"coolinvert": configData.coolinvert,
|
||||
"coolminutes": parseFloat(cmIn.val()),
|
||||
"cooldisable": !con.is(":checked"),
|
||||
"verbose": configData.verbose
|
||||
}])
|
||||
})
|
||||
window.setInterval(renderThermostats, 60000);
|
||||
});
|
||||
window.clearInterval(rtHandle);
|
||||
rtHandle = window.setInterval(renderThermostats, 60000);
|
||||
renderThermostats();
|
||||
});
|
||||
|
||||
var noButton = $("<button></button>").addClass("button").text("✘").click(function() {
|
||||
window.setInterval(renderThermostats, 60000);
|
||||
window.clearInterval(rtHandle);
|
||||
rtHandle = window.setInterval(renderThermostats, 60000);
|
||||
renderThermostats();
|
||||
});
|
||||
|
||||
if (!configData.heatdisable || !configData.cooldisable) {
|
||||
var buttonDiv = $("<div></div>").addClass("three columns").append(yesButton).append(noButton);
|
||||
rowdiv.append(buttonDiv);
|
||||
}
|
||||
var buttonDiv = $("<div></div>").addClass("two columns").append(yesButton).append($("<br>")).append(noButton);
|
||||
rowdiv.append(buttonDiv);
|
||||
|
||||
// Add things back to the thermostat list
|
||||
$("#thermostats").append(titlediv);
|
||||
|
|
@ -180,8 +208,16 @@ function renderThermostats() {
|
|||
beforeSend: authHeaders
|
||||
}).then(function(data) {
|
||||
$("#thermostats").empty();
|
||||
for (var key in data) {
|
||||
appendData(data[key])
|
||||
|
||||
// Sort by sensor alias
|
||||
var sorted = [];
|
||||
for(var key in data) {
|
||||
sorted[sorted.length] = key;
|
||||
}
|
||||
sorted.sort();
|
||||
|
||||
for (var i in sorted) {
|
||||
appendData(data[sorted[i]])
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
|||
26
influx_test.go
Normal file
26
influx_test.go
Normal 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)
|
||||
}
|
||||
2
main.go
2
main.go
|
|
@ -8,7 +8,7 @@ import (
|
|||
)
|
||||
|
||||
// Version is the current code version of tempgopher
|
||||
const Version = "0.3.1"
|
||||
const Version = "0.4.0"
|
||||
|
||||
func main() {
|
||||
var args struct {
|
||||
|
|
|
|||
|
|
@ -24,5 +24,4 @@ sensors:
|
|||
users:
|
||||
- foo: bar
|
||||
baseurl: https://foo.bar
|
||||
listenaddr: 127.0.0.1:8080
|
||||
displayfahrenheit: true
|
||||
|
|
|
|||
|
|
@ -24,5 +24,4 @@ sensors:
|
|||
users:
|
||||
- foo: bar
|
||||
baseurl: https://foo.bar
|
||||
listenaddr: 127.0.0.1:8080
|
||||
displayfahrenheit: true
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ users:
|
|||
- name: foo
|
||||
password: bar
|
||||
baseurl: https://foo.bar
|
||||
listenaddr: 127.0.0.1:8080
|
||||
displayfahrenheit: true
|
||||
influx:
|
||||
addr: http://foo:8086
|
||||
|
|
|
|||
13
web.go
13
web.go
|
|
@ -104,6 +104,15 @@ func VersionHandler(c *gin.Context) {
|
|||
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.
|
||||
func SetupRouter(config *Config, states *map[string]State) *gin.Engine {
|
||||
// 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())
|
||||
|
||||
// Redirect / to /app
|
||||
r.Any("/", func(c *gin.Context) {
|
||||
c.Redirect(301, config.BaseURL+"/app/")
|
||||
})
|
||||
r.Any("/", AppHandler(config))
|
||||
|
||||
return r
|
||||
}
|
||||
|
|
|
|||
290
web_test.go
Normal file
290
web_test.go
Normal 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)
|
||||
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue