diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8683e89..727d050 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,12 +25,12 @@ If you're working on the frontend and can use mocked data rather than a real bac ``` cd webapp/frontend npm install -ng serve +ng serve --deploy-url="/web/" --base-href="/web/" ``` However, if you need to also run the backend, and use real data, you'll need to run the following command: ``` -cd webapp/frontend && ng build --watch --output-path=../../dist --deploy-url="/web/" --base-href="/web/" --prod +cd webapp/frontend && ng build --watch --output-path=../../dist --prod ``` > Note: if you do not add `--prod` flag, app will display mocked data for api calls. diff --git a/collector/cmd/collector-metrics/collector-metrics.go b/collector/cmd/collector-metrics/collector-metrics.go index d06be83..14782e0 100644 --- a/collector/cmd/collector-metrics/collector-metrics.go +++ b/collector/cmd/collector-metrics/collector-metrics.go @@ -10,6 +10,7 @@ import ( "io" "log" "os" + "strings" "time" utils "github.com/analogj/go-util/utils" @@ -113,7 +114,10 @@ OPTIONS: } if c.IsSet("api-endpoint") { - config.Set("api.endpoint", c.String("api-endpoint")) + //if the user is providing an api-endpoint with a basepath (eg. http://localhost:8080/scrutiny), + //we need to ensure the basepath has a trailing slash, otherwise the url.Parse() path concatenation doesnt work. + apiEndpoint := strings.TrimSuffix(c.String("api-endpoint"), "/") + "/" + config.Set("api.endpoint", apiEndpoint) } collectorLogger := logrus.WithFields(logrus.Fields{ diff --git a/collector/pkg/collector/metrics.go b/collector/pkg/collector/metrics.go index 656aaad..31b6af7 100644 --- a/collector/pkg/collector/metrics.go +++ b/collector/pkg/collector/metrics.go @@ -48,7 +48,7 @@ func (mc *MetricsCollector) Run() error { } apiEndpoint, _ := url.Parse(mc.apiEndpoint.String()) - apiEndpoint.Path = "/api/devices/register" + apiEndpoint, _ = apiEndpoint.Parse("api/devices/register") //this acts like filepath.Join() deviceRespWrapper := new(models.DeviceWrapper) @@ -73,6 +73,7 @@ func (mc *MetricsCollector) Run() error { if !deviceRespWrapper.Success { mc.logger.Errorln("An error occurred while retrieving filtered devices") + mc.logger.Debugln(deviceRespWrapper) return errors.ApiServerCommunicationError("An error occurred while retrieving filtered devices") } else { mc.logger.Debugln(deviceRespWrapper) @@ -146,7 +147,7 @@ func (mc *MetricsCollector) Publish(deviceWWN string, payload []byte) error { mc.logger.Infof("Publishing smartctl results for %s\n", deviceWWN) apiEndpoint, _ := url.Parse(mc.apiEndpoint.String()) - apiEndpoint.Path = fmt.Sprintf("/api/device/%s/smart", strings.ToLower(deviceWWN)) + apiEndpoint, _ = apiEndpoint.Parse(fmt.Sprintf("api/device/%s/smart", strings.ToLower(deviceWWN))) resp, err := httpClient.Post(apiEndpoint.String(), "application/json", bytes.NewBuffer(payload)) if err != nil { diff --git a/collector/pkg/collector/metrics_test.go b/collector/pkg/collector/metrics_test.go new file mode 100644 index 0000000..f2bc407 --- /dev/null +++ b/collector/pkg/collector/metrics_test.go @@ -0,0 +1,38 @@ +package collector + +import ( + "github.com/stretchr/testify/require" + "net/url" + "testing" +) + +func TestApiEndpointParse(t *testing.T) { + baseURL, _ := url.Parse("http://localhost:8080/") + + url1, _ := baseURL.Parse("d/e") + require.Equal(t, "http://localhost:8080/d/e", url1.String()) + + url2, _ := baseURL.Parse("/d/e") + require.Equal(t, "http://localhost:8080/d/e", url2.String()) +} + +func TestApiEndpointParse_WithBasepathWithoutTrailingSlash(t *testing.T) { + baseURL, _ := url.Parse("http://localhost:8080/scrutiny") + + //This testcase is unexpected and can cause issues. We need to ensure the apiEndpoint always has a trailing slash. + url1, _ := baseURL.Parse("d/e") + require.Equal(t, "http://localhost:8080/d/e", url1.String()) + + url2, _ := baseURL.Parse("/d/e") + require.Equal(t, "http://localhost:8080/d/e", url2.String()) +} + +func TestApiEndpointParse_WithBasepathWithTrailingSlash(t *testing.T) { + baseURL, _ := url.Parse("http://localhost:8080/scrutiny/") + + url1, _ := baseURL.Parse("d/e") + require.Equal(t, "http://localhost:8080/scrutiny/d/e", url1.String()) + + url2, _ := baseURL.Parse("/d/e") + require.Equal(t, "http://localhost:8080/d/e", url2.String()) +} diff --git a/docker/Dockerfile b/docker/Dockerfile index 46b6bf9..06d13a5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ COPY webapp/frontend /opt/scrutiny/src RUN npm install -g @angular/cli@9.1.4 && \ mkdir -p /scrutiny/dist && \ npm install && \ - ng build --output-path=/opt/scrutiny/dist --deploy-url="/web/" --base-href="/web/" --prod + ng build --output-path=/opt/scrutiny/dist --prod ######## diff --git a/docker/Dockerfile.web b/docker/Dockerfile.web index 8307f01..f4e928e 100644 --- a/docker/Dockerfile.web +++ b/docker/Dockerfile.web @@ -20,7 +20,7 @@ COPY webapp/frontend /opt/scrutiny/src RUN npm install -g @angular/cli@9.1.4 && \ mkdir -p /opt/scrutiny/dist && \ npm install && \ - ng build --output-path=/opt/scrutiny/dist --deploy-url="/web/" --base-href="/web/" --prod + ng build --output-path=/opt/scrutiny/dist --prod ######## diff --git a/example.scrutiny.yaml b/example.scrutiny.yaml index 587885b..8e08705 100644 --- a/example.scrutiny.yaml +++ b/example.scrutiny.yaml @@ -20,10 +20,18 @@ web: listen: port: 8080 host: 0.0.0.0 + + # if you're using a reverse proxy like apache/nginx, you can override this value to serve scrutiny on a subpath. + # eg. http://example.com/scrutiny/* vs http://example.com:8080 + # see docs/TROUBLESHOOTING_REVERSE_PROXY.md + # basepath: `/scrutiny` + # leave empty unless behind a path prefixed proxy + basepath: '' database: # can also set absolute path here location: /opt/scrutiny/config/scrutiny.db src: + # the location on the filesystem where scrutiny javascript + css is located frontend: path: /opt/scrutiny/web influxdb: diff --git a/go.mod b/go.mod index ebc234b..391441f 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14 + github.com/citilinkru/libudev v1.0.0 github.com/containrrr/shoutrrr v0.4.4 github.com/fatih/color v1.10.0 github.com/gin-gonic/gin v1.6.3 diff --git a/webapp/backend/pkg/config/config.go b/webapp/backend/pkg/config/config.go index 71de1be..fc5f0d9 100644 --- a/webapp/backend/pkg/config/config.go +++ b/webapp/backend/pkg/config/config.go @@ -30,6 +30,7 @@ func (c *configuration) Init() error { //set defaults c.SetDefault("web.listen.port", "8080") c.SetDefault("web.listen.host", "0.0.0.0") + c.SetDefault("web.listen.basepath", "") c.SetDefault("web.src.frontend.path", "/opt/scrutiny/web") c.SetDefault("web.database.location", "/opt/scrutiny/config/scrutiny.db") diff --git a/webapp/backend/pkg/web/middleware/logger.go b/webapp/backend/pkg/web/middleware/logger.go index 3980469..dc988bb 100644 --- a/webapp/backend/pkg/web/middleware/logger.go +++ b/webapp/backend/pkg/web/middleware/logger.go @@ -89,7 +89,7 @@ func LoggerMiddleware(logger logrus.FieldLogger) gin.HandlerFunc { entry.Info(msg) } } - if strings.HasPrefix(path, "/api/") { + if strings.Contains(path, "/api/") { //only debug log request/response from api endpoint. if len(reqBody) > 0 { entry.WithField("bodyType", "request").Debugln(reqBody) // Print request body diff --git a/webapp/backend/pkg/web/server.go b/webapp/backend/pkg/web/server.go index 078e216..974ffb2 100644 --- a/webapp/backend/pkg/web/server.go +++ b/webapp/backend/pkg/web/server.go @@ -27,29 +27,35 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine { r.Use(middleware.ConfigMiddleware(ae.Config)) r.Use(gin.Recovery()) - api := r.Group("/api") + basePath := ae.Config.GetString("web.listen.basepath") + logger.Debugf("basepath: %s", basePath) + + base := r.Group(basePath) { - api.GET("/health", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "success": true, + api := base.Group("/api") + { + api.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "success": true, + }) }) - }) - api.POST("/health/notify", handler.SendTestNotification) //check if notifications are configured correctly - - api.POST("/devices/register", handler.RegisterDevices) //used by Collector to register new devices and retrieve filtered list - api.GET("/summary", handler.GetDevicesSummary) //used by Dashboard - api.GET("/summary/temp", handler.GetDevicesSummaryTempHistory) //used by Dashboard (Temperature history dropdown) - api.POST("/device/:wwn/smart", handler.UploadDeviceMetrics) //used by Collector to upload data - api.POST("/device/:wwn/selftest", handler.UploadDeviceSelfTests) - api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details + api.POST("/health/notify", handler.SendTestNotification) //check if notifications are configured correctly + + api.POST("/devices/register", handler.RegisterDevices) //used by Collector to register new devices and retrieve filtered list + api.GET("/summary", handler.GetDevicesSummary) //used by Dashboard + api.GET("/summary/temp", handler.GetDevicesSummaryTempHistory) //used by Dashboard (Temperature history dropdown) + api.POST("/device/:wwn/smart", handler.UploadDeviceMetrics) //used by Collector to upload data + api.POST("/device/:wwn/selftest", handler.UploadDeviceSelfTests) + api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details + } } //Static request routing - r.StaticFS("/web", http.Dir(ae.Config.GetString("web.src.frontend.path"))) + base.StaticFS("/web", http.Dir(ae.Config.GetString("web.src.frontend.path"))) //redirect base url to /web - r.GET("/", func(c *gin.Context) { - c.Redirect(http.StatusFound, "/web") + base.GET("/", func(c *gin.Context) { + c.Redirect(http.StatusFound, basePath+"/web") }) //catch-all, serve index page. diff --git a/webapp/backend/pkg/web/server_test.go b/webapp/backend/pkg/web/server_test.go index b7816d8..c8400dd 100644 --- a/webapp/backend/pkg/web/server_test.go +++ b/webapp/backend/pkg/web/server_test.go @@ -11,6 +11,7 @@ import ( "github.com/golang/mock/gomock" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" "io" "io/ioutil" "net/http" @@ -61,15 +62,36 @@ func helperReadSmartDataFileFixTimestamp(t *testing.T, smartDataFilepath string) return bytes.NewReader(updatedSmartDataBytes) } -func TestHealthRoute(t *testing.T) { +// Define the suite, and absorb the built-in basic suite +// functionality from testify - including a T() method which +// returns the current testing context +type ServerTestSuite struct { + suite.Suite + Basepath string +} + +func TestServerTestSuite_WithEmptyBasePath(t *testing.T) { + emptyBasePathSuite := new(ServerTestSuite) + emptyBasePathSuite.Basepath = "" + suite.Run(t, emptyBasePathSuite) +} + +func TestServerTestSuite_WithCustomBasePath(t *testing.T) { + emptyBasePathSuite := new(ServerTestSuite) + emptyBasePathSuite.Basepath = "/basepath" + suite.Run(t, emptyBasePathSuite) +} + +func (suite *ServerTestSuite) TestHealthRoute() { //setup parentPath, _ := ioutil.TempDir("", "") defer os.RemoveAll(parentPath) - mockCtrl := gomock.NewController(t) + mockCtrl := gomock.NewController(suite.T()) defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig.EXPECT().GetString("web.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes() fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes() + fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes() fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes() @@ -92,23 +114,24 @@ func TestHealthRoute(t *testing.T) { //test w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/health", nil) + req, _ := http.NewRequest("GET", suite.Basepath+"/api/health", nil) router.ServeHTTP(w, req) //assert - require.Equal(t, 200, w.Code) - require.Equal(t, "{\"success\":true}", w.Body.String()) + require.Equal(suite.T(), 200, w.Code) + require.Equal(suite.T(), "{\"success\":true}", w.Body.String()) } -func TestRegisterDevicesRoute(t *testing.T) { +func (suite *ServerTestSuite) TestRegisterDevicesRoute() { //setup parentPath, _ := ioutil.TempDir("", "") defer os.RemoveAll(parentPath) - mockCtrl := gomock.NewController(t) + mockCtrl := gomock.NewController(suite.T()) defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig.EXPECT().GetString("web.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes() fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes() + fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes() fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() @@ -127,26 +150,27 @@ func TestRegisterDevicesRoute(t *testing.T) { } router := ae.Setup(logrus.New()) file, err := os.Open("testdata/register-devices-req.json") - require.NoError(t, err) + require.NoError(suite.T(), err) //test w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/devices/register", file) + req, _ := http.NewRequest("POST", suite.Basepath+"/api/devices/register", file) router.ServeHTTP(w, req) //assert - require.Equal(t, 200, w.Code) + require.Equal(suite.T(), 200, w.Code) } -func TestUploadDeviceMetricsRoute(t *testing.T) { +func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() { //setup parentPath, _ := ioutil.TempDir("", "") defer os.RemoveAll(parentPath) - mockCtrl := gomock.NewController(t) + mockCtrl := gomock.NewController(suite.T()) defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db")) fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath) + fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes() fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() @@ -165,35 +189,36 @@ func TestUploadDeviceMetricsRoute(t *testing.T) { } router := ae.Setup(logrus.New()) devicesfile, err := os.Open("testdata/register-devices-single-req.json") - require.NoError(t, err) + require.NoError(suite.T(), err) - metricsfile := helperReadSmartDataFileFixTimestamp(t, "testdata/upload-device-metrics-req.json") + metricsfile := helperReadSmartDataFileFixTimestamp(suite.T(), "testdata/upload-device-metrics-req.json") //test wr := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/devices/register", devicesfile) + req, _ := http.NewRequest("POST", suite.Basepath+"/api/devices/register", devicesfile) router.ServeHTTP(wr, req) - require.Equal(t, 200, wr.Code) + require.Equal(suite.T(), 200, wr.Code) mr := httptest.NewRecorder() - req, _ = http.NewRequest("POST", "/api/device/0x5000cca264eb01d7/smart", metricsfile) + req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264eb01d7/smart", metricsfile) router.ServeHTTP(mr, req) - require.Equal(t, 200, mr.Code) + require.Equal(suite.T(), 200, mr.Code) //assert } -func TestPopulateMultiple(t *testing.T) { +func (suite *ServerTestSuite) TestPopulateMultiple() { //setup parentPath, _ := ioutil.TempDir("", "") defer os.RemoveAll(parentPath) - mockCtrl := gomock.NewController(t) + mockCtrl := gomock.NewController(suite.T()) defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) //fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return("testdata/scrutiny_test.db") fakeConfig.EXPECT().GetStringSlice("notify.urls").Return([]string{}).AnyTimes() fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db")) fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath) + fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes() fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() @@ -212,44 +237,44 @@ func TestPopulateMultiple(t *testing.T) { } router := ae.Setup(logrus.New()) devicesfile, err := os.Open("testdata/register-devices-req.json") - require.NoError(t, err) + require.NoError(suite.T(), err) - metricsfile := helperReadSmartDataFileFixTimestamp(t, "../models/testdata/smart-ata.json") - failfile := helperReadSmartDataFileFixTimestamp(t, "../models/testdata/smart-fail2.json") - nvmefile := helperReadSmartDataFileFixTimestamp(t, "../models/testdata/smart-nvme.json") - scsifile := helperReadSmartDataFileFixTimestamp(t, "../models/testdata/smart-scsi.json") - scsi2file := helperReadSmartDataFileFixTimestamp(t, "../models/testdata/smart-scsi2.json") + metricsfile := helperReadSmartDataFileFixTimestamp(suite.T(), "../models/testdata/smart-ata.json") + failfile := helperReadSmartDataFileFixTimestamp(suite.T(), "../models/testdata/smart-fail2.json") + nvmefile := helperReadSmartDataFileFixTimestamp(suite.T(), "../models/testdata/smart-nvme.json") + scsifile := helperReadSmartDataFileFixTimestamp(suite.T(), "../models/testdata/smart-scsi.json") + scsi2file := helperReadSmartDataFileFixTimestamp(suite.T(), "../models/testdata/smart-scsi2.json") //test wr := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/devices/register", devicesfile) + req, _ := http.NewRequest("POST", suite.Basepath+"/api/devices/register", devicesfile) router.ServeHTTP(wr, req) - require.Equal(t, 200, wr.Code) + require.Equal(suite.T(), 200, wr.Code) mr := httptest.NewRecorder() - req, _ = http.NewRequest("POST", "/api/device/0x5000cca264eb01d7/smart", metricsfile) + req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264eb01d7/smart", metricsfile) router.ServeHTTP(mr, req) - require.Equal(t, 200, mr.Code) + require.Equal(suite.T(), 200, mr.Code) fr := httptest.NewRecorder() - req, _ = http.NewRequest("POST", "/api/device/0x5000cca264ec3183/smart", failfile) + req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264ec3183/smart", failfile) router.ServeHTTP(fr, req) - require.Equal(t, 200, fr.Code) + require.Equal(suite.T(), 200, fr.Code) nr := httptest.NewRecorder() - req, _ = http.NewRequest("POST", "/api/device/0x5002538e40a22954/smart", nvmefile) + req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5002538e40a22954/smart", nvmefile) router.ServeHTTP(nr, req) - require.Equal(t, 200, nr.Code) + require.Equal(suite.T(), 200, nr.Code) sr := httptest.NewRecorder() - req, _ = http.NewRequest("POST", "/api/device/0x5000cca252c859cc/smart", scsifile) + req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca252c859cc/smart", scsifile) router.ServeHTTP(sr, req) - require.Equal(t, 200, sr.Code) + require.Equal(suite.T(), 200, sr.Code) s2r := httptest.NewRecorder() - req, _ = http.NewRequest("POST", "/api/device/0x5000cca264ebc248/smart", scsi2file) + req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264ebc248/smart", scsi2file) router.ServeHTTP(s2r, req) - require.Equal(t, 200, s2r.Code) + require.Equal(suite.T(), 200, s2r.Code) //assert } @@ -279,15 +304,16 @@ func TestPopulateMultiple(t *testing.T) { // require.Equal(t, 200, wr.Code) //} -func TestSendTestNotificationRoute_WebhookFailure(t *testing.T) { +func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() { //setup parentPath, _ := ioutil.TempDir("", "") defer os.RemoveAll(parentPath) - mockCtrl := gomock.NewController(t) + mockCtrl := gomock.NewController(suite.T()) defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db")) fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath) + fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes() fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() @@ -309,22 +335,23 @@ func TestSendTestNotificationRoute_WebhookFailure(t *testing.T) { //test wr := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/health/notify", strings.NewReader("{}")) + req, _ := http.NewRequest("POST", suite.Basepath+"/api/health/notify", strings.NewReader("{}")) router.ServeHTTP(wr, req) //assert - require.Equal(t, 500, wr.Code) + require.Equal(suite.T(), 500, wr.Code) } -func TestSendTestNotificationRoute_ScriptFailure(t *testing.T) { +func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() { //setup parentPath, _ := ioutil.TempDir("", "") defer os.RemoveAll(parentPath) - mockCtrl := gomock.NewController(t) + mockCtrl := gomock.NewController(suite.T()) defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db")) fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath) + fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes() fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() @@ -346,22 +373,23 @@ func TestSendTestNotificationRoute_ScriptFailure(t *testing.T) { //test wr := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/health/notify", strings.NewReader("{}")) + req, _ := http.NewRequest("POST", suite.Basepath+"/api/health/notify", strings.NewReader("{}")) router.ServeHTTP(wr, req) //assert - require.Equal(t, 500, wr.Code) + require.Equal(suite.T(), 500, wr.Code) } -func TestSendTestNotificationRoute_ScriptSuccess(t *testing.T) { +func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() { //setup parentPath, _ := ioutil.TempDir("", "") defer os.RemoveAll(parentPath) - mockCtrl := gomock.NewController(t) + mockCtrl := gomock.NewController(suite.T()) defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db")) fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath) + fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes() fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() @@ -383,22 +411,23 @@ func TestSendTestNotificationRoute_ScriptSuccess(t *testing.T) { //test wr := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/health/notify", strings.NewReader("{}")) + req, _ := http.NewRequest("POST", suite.Basepath+"/api/health/notify", strings.NewReader("{}")) router.ServeHTTP(wr, req) //assert - require.Equal(t, 200, wr.Code) + require.Equal(suite.T(), 200, wr.Code) } -func TestSendTestNotificationRoute_ShoutrrrFailure(t *testing.T) { +func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() { //setup parentPath, _ := ioutil.TempDir("", "") defer os.RemoveAll(parentPath) - mockCtrl := gomock.NewController(t) + mockCtrl := gomock.NewController(suite.T()) defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db")) fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath) + fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes() fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() @@ -419,22 +448,23 @@ func TestSendTestNotificationRoute_ShoutrrrFailure(t *testing.T) { //test wr := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/health/notify", strings.NewReader("{}")) + req, _ := http.NewRequest("POST", suite.Basepath+"/api/health/notify", strings.NewReader("{}")) router.ServeHTTP(wr, req) //assert - require.Equal(t, 500, wr.Code) + require.Equal(suite.T(), 500, wr.Code) } -func TestGetDevicesSummaryRoute_Nvme(t *testing.T) { +func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() { //setup parentPath, _ := ioutil.TempDir("", "") defer os.RemoveAll(parentPath) - mockCtrl := gomock.NewController(t) + mockCtrl := gomock.NewController(suite.T()) defer mockCtrl.Finish() fakeConfig := mock_config.NewMockInterface(mockCtrl) fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db")) fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath) + fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes() fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes() @@ -454,30 +484,30 @@ func TestGetDevicesSummaryRoute_Nvme(t *testing.T) { } router := ae.Setup(logrus.New()) devicesfile, err := os.Open("testdata/register-devices-req-2.json") - require.NoError(t, err) + require.NoError(suite.T(), err) - metricsfile := helperReadSmartDataFileFixTimestamp(t, "../models/testdata/smart-nvme2.json") + metricsfile := helperReadSmartDataFileFixTimestamp(suite.T(), "../models/testdata/smart-nvme2.json") //test wr := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/devices/register", devicesfile) + req, _ := http.NewRequest("POST", suite.Basepath+"/api/devices/register", devicesfile) router.ServeHTTP(wr, req) - require.Equal(t, 200, wr.Code) + require.Equal(suite.T(), 200, wr.Code) mr := httptest.NewRecorder() - req, _ = http.NewRequest("POST", "/api/device/a4c8e8ed-11a0-4c97-9bba-306440f1b944/smart", metricsfile) + req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/a4c8e8ed-11a0-4c97-9bba-306440f1b944/smart", metricsfile) router.ServeHTTP(mr, req) - require.Equal(t, 200, mr.Code) + require.Equal(suite.T(), 200, mr.Code) sr := httptest.NewRecorder() - req, _ = http.NewRequest("GET", "/api/summary", nil) + req, _ = http.NewRequest("GET", suite.Basepath+"/api/summary", nil) router.ServeHTTP(sr, req) - require.Equal(t, 200, sr.Code) + require.Equal(suite.T(), 200, sr.Code) var deviceSummary models.DeviceSummaryWrapper err = json.Unmarshal(sr.Body.Bytes(), &deviceSummary) - require.NoError(t, err) + require.NoError(suite.T(), err) //assert - require.Equal(t, "a4c8e8ed-11a0-4c97-9bba-306440f1b944", deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.WWN) - require.Equal(t, pkg.DeviceStatusFailedScrutiny, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus) + require.Equal(suite.T(), "a4c8e8ed-11a0-4c97-9bba-306440f1b944", deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.WWN) + require.Equal(suite.T(), pkg.DeviceStatusFailedScrutiny, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus) } diff --git a/webapp/frontend/src/app/app.module.ts b/webapp/frontend/src/app/app.module.ts index 9b5ee3a..904ee15 100644 --- a/webapp/frontend/src/app/app.module.ts +++ b/webapp/frontend/src/app/app.module.ts @@ -2,6 +2,7 @@ import { NgModule, enableProdMode } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ExtraOptions, PreloadAllModules, RouterModule } from '@angular/router'; +import { APP_BASE_HREF } from '@angular/common'; import { MarkdownModule } from 'ngx-markdown'; import { TreoModule } from '@treo'; import { TreoConfigModule } from '@treo/services/config'; @@ -11,7 +12,7 @@ import { appConfig } from 'app/core/config/app.config'; import { mockDataServices } from 'app/data/mock'; import { LayoutModule } from 'app/layout/layout.module'; import { AppComponent } from 'app/app.component'; -import { appRoutes } from 'app/app.routing'; +import { appRoutes, getAppBaseHref } from 'app/app.routing'; const routerConfig: ExtraOptions = { scrollPositionRestoration: 'enabled', @@ -54,7 +55,13 @@ if (process.env.NODE_ENV === 'production') { ], bootstrap : [ AppComponent - ] + ], + providers: [ + { + provide: APP_BASE_HREF, + useValue: getAppBaseHref() + } + ], }) export class AppModule { diff --git a/webapp/frontend/src/app/app.routing.ts b/webapp/frontend/src/app/app.routing.ts index f7ad8bc..2be8dd2 100644 --- a/webapp/frontend/src/app/app.routing.ts +++ b/webapp/frontend/src/app/app.routing.ts @@ -2,6 +2,17 @@ import { Route } from '@angular/router'; import { LayoutComponent } from 'app/layout/layout.component'; import { EmptyLayoutComponent } from 'app/layout/layouts/empty/empty.component'; +// @formatter:off +export function getAppBaseHref(): string { + return getBasePath() + '/web'; +} + +// @formatter:off +// tslint:disable:max-line-length +export function getBasePath(): string { + return window.location.pathname.split('/web').slice(0, 1)[0]; +} + // @formatter:off // tslint:disable:max-line-length export const appRoutes: Route[] = [ diff --git a/webapp/frontend/src/app/layout/common/search/search.component.ts b/webapp/frontend/src/app/layout/common/search/search.component.ts index 56a824f..3c48651 100644 --- a/webapp/frontend/src/app/layout/common/search/search.component.ts +++ b/webapp/frontend/src/app/layout/common/search/search.component.ts @@ -5,6 +5,7 @@ import { MatFormField } from '@angular/material/form-field'; import { Subject } from 'rxjs'; import { debounceTime, filter, map, takeUntil } from 'rxjs/operators'; import { TreoAnimations } from '@treo/animations/public-api'; +import { getBasePath } from 'app/app.routing'; @Component({ selector : 'search', @@ -199,7 +200,7 @@ export class SearchComponent implements OnInit, OnDestroy }) ) .subscribe((value) => { - this._httpClient.post('api/common/search', {query: value}) + this._httpClient.post(getBasePath() + '/api/common/search', {query: value}) .subscribe((response: any) => { this.results = response.results; }); diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts index bd81204..f73704c 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { BehaviorSubject, Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; +import { getBasePath } from 'app/app.routing'; @Injectable({ providedIn: 'root' @@ -45,7 +46,7 @@ export class DashboardService */ getData(): Observable { - return this._httpClient.get('/api/summary').pipe( + return this._httpClient.get(getBasePath() + '/api/summary').pipe( tap((response: any) => { this._data.next(response); }) diff --git a/webapp/frontend/src/app/modules/detail/detail.service.ts b/webapp/frontend/src/app/modules/detail/detail.service.ts index 12e0a59..5747571 100644 --- a/webapp/frontend/src/app/modules/detail/detail.service.ts +++ b/webapp/frontend/src/app/modules/detail/detail.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { BehaviorSubject, Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; +import { getBasePath } from 'app/app.routing'; @Injectable({ providedIn: 'root' @@ -45,7 +46,7 @@ export class DetailService */ getData(wwn): Observable { - return this._httpClient.get(`/api/device/${wwn}/details`).pipe( + return this._httpClient.get(getBasePath() + `/api/device/${wwn}/details`).pipe( tap((response: any) => { this._data.next(response); }) diff --git a/webapp/frontend/src/browserconfig.xml b/webapp/frontend/src/browserconfig.xml index c554148..ddcadc8 100644 --- a/webapp/frontend/src/browserconfig.xml +++ b/webapp/frontend/src/browserconfig.xml @@ -1,2 +1,2 @@ -#ffffff \ No newline at end of file +#ffffff diff --git a/webapp/frontend/src/index.html b/webapp/frontend/src/index.html index 1578a30..56dca49 100644 --- a/webapp/frontend/src/index.html +++ b/webapp/frontend/src/index.html @@ -3,7 +3,6 @@ scrutiny - @@ -22,7 +21,7 @@ - + diff --git a/webapp/frontend/src/manifest.json b/webapp/frontend/src/manifest.json index 013d4a6..a6ee00c 100644 --- a/webapp/frontend/src/manifest.json +++ b/webapp/frontend/src/manifest.json @@ -2,40 +2,40 @@ "name": "App", "icons": [ { - "src": "\/android-icon-36x36.png", + "src": ".\/android-icon-36x36.png", "sizes": "36x36", "type": "image\/png", "density": "0.75" }, { - "src": "\/android-icon-48x48.png", + "src": ".\/android-icon-48x48.png", "sizes": "48x48", "type": "image\/png", "density": "1.0" }, { - "src": "\/android-icon-72x72.png", + "src": ".\/android-icon-72x72.png", "sizes": "72x72", "type": "image\/png", "density": "1.5" }, { - "src": "\/android-icon-96x96.png", + "src": ".\/android-icon-96x96.png", "sizes": "96x96", "type": "image\/png", "density": "2.0" }, { - "src": "\/android-icon-144x144.png", + "src": ".\/android-icon-144x144.png", "sizes": "144x144", "type": "image\/png", "density": "3.0" }, { - "src": "\/android-icon-192x192.png", + "src": ".\/android-icon-192x192.png", "sizes": "192x192", "type": "image\/png", "density": "4.0" } ] -} \ No newline at end of file +}