From 4c5bd9a7be002ca7eb38dd3516433c0e2f2c08a1 Mon Sep 17 00:00:00 2001 From: Amir Raminfar Date: Tue, 31 Jan 2023 12:03:41 -0800 Subject: [PATCH] Reverts back previous fix and uses proper error handeling (#2029) * Reverts back previous fix and uses proper error handeling * Adds more tests and refactors existing tests --- docker/log_iterator.go | 52 +++-- package.json | 2 +- web/__snapshots__/web.snapshot | 8 + web/logs.go | 14 +- web/routes_auth_test.go | 214 ++++++++++++++++++ web/routes_logs_test.go | 223 +++++++++++++++++++ web/routes_test.go | 392 +-------------------------------- 7 files changed, 487 insertions(+), 418 deletions(-) create mode 100644 web/routes_auth_test.go create mode 100644 web/routes_logs_test.go diff --git a/docker/log_iterator.go b/docker/log_iterator.go index f1457f17..8367d13b 100644 --- a/docker/log_iterator.go +++ b/docker/log_iterator.go @@ -32,8 +32,12 @@ func (g *eventGenerator) Next() (*LogEvent, error) { g.next = nil nextEvent = g.Peek() } else { - currentEvent = <-g.channel + event, ok := <-g.channel + if !ok { + return nil, g.lastError + } + currentEvent = event nextEvent = g.Peek() } @@ -62,7 +66,7 @@ func (g *eventGenerator) Next() (*LogEvent, error) { currentEvent.Position = END } - return currentEvent, g.lastError + return currentEvent, nil } func (g *eventGenerator) LastError() error { @@ -86,36 +90,36 @@ func (g *eventGenerator) consume() { for { message, readerError := g.reader.ReadString('\n') - h := fnv.New32a() - h.Write([]byte(message)) + if message != "" { + h := fnv.New32a() + h.Write([]byte(message)) - logEvent := &LogEvent{Id: h.Sum32(), Message: message} + logEvent := &LogEvent{Id: h.Sum32(), Message: message} - if index := strings.IndexAny(message, " "); index != -1 { - logId := message[:index] - if timestamp, err := time.Parse(time.RFC3339Nano, logId); err == nil { - logEvent.Timestamp = timestamp.UnixMilli() - message = strings.TrimSuffix(message[index+1:], "\n") - logEvent.Message = message - if strings.HasPrefix(message, "{") && strings.HasSuffix(message, "}") { - var data map[string]interface{} - if err := json.Unmarshal([]byte(message), &data); err != nil { - log.Errorf("json unmarshal error while streaming %v", err.Error()) - } else { - logEvent.Message = data + if index := strings.IndexAny(message, " "); index != -1 { + logId := message[:index] + if timestamp, err := time.Parse(time.RFC3339Nano, logId); err == nil { + logEvent.Timestamp = timestamp.UnixMilli() + message = strings.TrimSuffix(message[index+1:], "\n") + logEvent.Message = message + if strings.HasPrefix(message, "{") && strings.HasSuffix(message, "}") { + var data map[string]interface{} + if err := json.Unmarshal([]byte(message), &data); err != nil { + log.Errorf("json unmarshal error while streaming %v", err.Error()) + } else { + logEvent.Message = data + } } } } + logEvent.Level = guessLogLevel(logEvent) + g.channel <- logEvent } - - logEvent.Level = guessLogLevel(logEvent) - - g.channel <- logEvent - + if readerError != nil { - close(g.channel) g.lastError = readerError - break + close(g.channel) + return } } } diff --git a/package.json b/package.json index 98e60f82..c5defb0b 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "author": "Amir Raminfar ", "scripts": { "watch:assets": "vite --open", - "watch:server": "LIVE_FS=true DOZZLE_ADDR=:3100 reflex -c .reflex", + "watch:server": "LIVE_FS=true DOZZLE_ADDR=localhost:3100 reflex -c .reflex", "dev": "make fake_assets && npm-run-all -p watch:assets watch:server", "build": "vite build", "release": "release-it", diff --git a/web/__snapshots__/web.snapshot b/web/__snapshots__/web.snapshot index 9b35e1ff..7193d642 100644 --- a/web/__snapshots__/web.snapshot +++ b/web/__snapshots__/web.snapshot @@ -71,6 +71,14 @@ Content-Type: text/html
dev
+/* snapshot: Test_handler_between_dates */ +HTTP/1.1 200 OK +Connection: close +Content-Type: application/ld+json; charset=UTF-8 + +{"m":"INFO Testing logs...","ts":1589396137772,"id":2908612274,"l":"info"} +{"m":"INFO Testing logs...","ts":1589396137772,"id":2908612274,"l":"info"} + /* snapshot: Test_handler_streamEvents_error */ HTTP/1.1 200 OK Connection: close diff --git a/web/logs.go b/web/logs.go index 021a8769..7d07ac2d 100644 --- a/web/logs.go +++ b/web/logs.go @@ -66,14 +66,13 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) for { logEvent, readerError := iterator.Next() + if readerError != nil { + break + } if err := json.NewEncoder(w).Encode(logEvent); err != nil { log.Errorf("json encoding error while streaming %v", err.Error()) } - - if readerError != nil { - break - } } } @@ -140,6 +139,9 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) { for { logEvent, err := iterator.Next() + if err != nil { + break + } if buf, err := json.Marshal(logEvent); err != nil { log.Errorf("json encoding error while streaming %v", err.Error()) @@ -151,10 +153,6 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) { } fmt.Fprintf(w, "\n") f.Flush() - if err != nil { - break - } - } log.Debugf("streaming stopped: %v", container.ID) diff --git a/web/routes_auth_test.go b/web/routes_auth_test.go new file mode 100644 index 00000000..b4d0651e --- /dev/null +++ b/web/routes_auth_test.go @@ -0,0 +1,214 @@ +package web + +import ( + "bytes" + + "io" + + "io/ioutil" + "mime/multipart" + "net/http" + "net/http/httptest" + + "strings" + "testing" + "time" + + "github.com/magiconair/properties/assert" + + "github.com/amir20/dozzle/docker" + "github.com/beme/abide" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/spf13/afero" +) + +func Test_createRoutes_index(t *testing.T) { + fs := afero.NewMemMapFs() + require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.") + handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/"}) + req, err := http.NewRequest("GET", "/", nil) + require.NoError(t, err, "NewRequest should not return an error.") + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) +} + +func Test_createRoutes_redirect(t *testing.T) { + fs := afero.NewMemMapFs() + require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.") + + handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar"}) + req, err := http.NewRequest("GET", "/foobar", nil) + require.NoError(t, err, "NewRequest should not return an error.") + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) +} + +func Test_createRoutes_redirect_with_auth(t *testing.T) { + fs := afero.NewMemMapFs() + require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.") + + handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar", Username: "amir", Password: "password"}) + req, err := http.NewRequest("GET", "/foobar/", nil) + require.NoError(t, err, "NewRequest should not return an error.") + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) +} + +func Test_createRoutes_foobar(t *testing.T) { + fs := afero.NewMemMapFs() + require.NoError(t, afero.WriteFile(fs, "index.html", []byte("foo page"), 0644), "WriteFile should have no error.") + handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar"}) + req, err := http.NewRequest("GET", "/foobar/", nil) + require.NoError(t, err, "NewRequest should not return an error.") + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) +} + +func Test_createRoutes_foobar_file(t *testing.T) { + fs := afero.NewMemMapFs() + require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.") + require.NoError(t, afero.WriteFile(fs, "test", []byte("test page"), 0644), "WriteFile should have no error.") + + handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar"}) + req, err := http.NewRequest("GET", "/foobar/test", nil) + require.NoError(t, err, "NewRequest should not return an error.") + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + assert.Equal(t, rr.Body.String(), "test page", "page doesn't match") +} + +func Test_createRoutes_version(t *testing.T) { + fs := afero.NewMemMapFs() + require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.") + handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/", Version: "dev"}) + req, err := http.NewRequest("GET", "/version", nil) + require.NoError(t, err, "NewRequest should not return an error.") + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) +} + +func Test_createRoutes_username_password(t *testing.T) { + + handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"}) + req, err := http.NewRequest("GET", "/", nil) + require.NoError(t, err, "NewRequest should not return an error.") + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) +} + +func Test_createRoutes_username_password_invalid(t *testing.T) { + handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"}) + req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil) + require.NoError(t, err, "NewRequest should not return an error.") + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) +} + +func Test_createRoutes_username_password_login_happy(t *testing.T) { + handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"}) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + fw, err := writer.CreateFormField("username") + require.NoError(t, err, "Creating field should not be error.") + _, err = io.Copy(fw, strings.NewReader("amir")) + require.NoError(t, err, "Copying field should not result in error.") + + fw, err = writer.CreateFormField("password") + require.NoError(t, err, "Creating field should not be error.") + _, err = io.Copy(fw, strings.NewReader("password")) + require.NoError(t, err, "Copying field should not result in error.") + + writer.Close() + + req, err := http.NewRequest("POST", "/api/validateCredentials", bytes.NewReader(body.Bytes())) + req.Header.Set("Content-Type", writer.FormDataContentType()) + + require.NoError(t, err, "NewRequest should not return an error.") + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.Equal(t, rr.Code, 200) + cookie := rr.Header().Get("Set-Cookie") + assert.Matches(t, cookie, "session=.+") +} + +func Test_createRoutes_username_password_login_failed(t *testing.T) { + handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"}) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + fw, err := writer.CreateFormField("username") + require.NoError(t, err, "Creating field should not be error.") + _, err = io.Copy(fw, strings.NewReader("amir")) + require.NoError(t, err, "Copying field should not result in error.") + + fw, err = writer.CreateFormField("password") + require.NoError(t, err, "Creating field should not be error.") + _, err = io.Copy(fw, strings.NewReader("bad")) + require.NoError(t, err, "Copying field should not result in error.") + + writer.Close() + + req, err := http.NewRequest("POST", "/api/validateCredentials", bytes.NewReader(body.Bytes())) + req.Header.Set("Content-Type", writer.FormDataContentType()) + + require.NoError(t, err, "NewRequest should not return an error.") + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + assert.Equal(t, rr.Code, 401) +} + +func Test_createRoutes_username_password_valid_session(t *testing.T) { + mockedClient := new(MockedClient) + mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil) + mockedClient.On("ContainerLogs", mock.Anything, "123", "").Return(ioutil.NopCloser(strings.NewReader("test data")), io.EOF) + handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"}) + + // Get cookie first + req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil) + require.NoError(t, err, "NewRequest should not return an error.") + session, _ := store.Get(req, sessionName) + session.Values[authorityKey] = time.Now().Unix() + recorder := httptest.NewRecorder() + session.Save(req, recorder) + cookies := recorder.Result().Cookies() + + // Test with cookie + req, err = http.NewRequest("GET", "/api/logs/stream?id=123", nil) + require.NoError(t, err, "NewRequest should not return an error.") + req.AddCookie(cookies[0]) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) +} + +func Test_createRoutes_username_password_invalid_session(t *testing.T) { + mockedClient := new(MockedClient) + mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil) + mockedClient.On("ContainerLogs", mock.Anything, "since").Return(ioutil.NopCloser(strings.NewReader("test data")), io.EOF) + handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"}) + req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil) + require.NoError(t, err, "NewRequest should not return an error.") + req.AddCookie(&http.Cookie{Name: "session", Value: "baddata"}) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + assert.Equal(t, rr.Code, 401) +} diff --git a/web/routes_logs_test.go b/web/routes_logs_test.go new file mode 100644 index 00000000..950636a7 --- /dev/null +++ b/web/routes_logs_test.go @@ -0,0 +1,223 @@ +package web + +import ( + "context" + "errors" + "io" + "time" + + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/amir20/dozzle/docker" + "github.com/beme/abide" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func Test_handler_streamLogs_happy(t *testing.T) { + id := "123456" + req, err := http.NewRequest("GET", "/api/logs/stream", nil) + q := req.URL.Query() + q.Add("id", id) + req.URL.RawQuery = q.Encode() + require.NoError(t, err, "NewRequest should not return an error.") + + mockedClient := new(MockedClient) + reader := ioutil.NopCloser(strings.NewReader("INFO Testing logs...")) + mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) + mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "").Return(reader, nil) + + h := handler{client: mockedClient, config: &Config{}} + handler := http.HandlerFunc(h.streamLogs) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) + mockedClient.AssertExpectations(t) +} + +func Test_handler_streamLogs_happy_with_id(t *testing.T) { + id := "123456" + req, err := http.NewRequest("GET", "/api/logs/stream", nil) + q := req.URL.Query() + q.Add("id", id) + req.URL.RawQuery = q.Encode() + require.NoError(t, err, "NewRequest should not return an error.") + + mockedClient := new(MockedClient) + reader := ioutil.NopCloser(strings.NewReader("2020-05-13T18:55:37.772853839Z INFO Testing logs...")) + mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) + mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "").Return(reader, nil) + + h := handler{client: mockedClient, config: &Config{}} + handler := http.HandlerFunc(h.streamLogs) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) + mockedClient.AssertExpectations(t) +} + +func Test_handler_streamLogs_happy_container_stopped(t *testing.T) { + id := "123456" + req, err := http.NewRequest("GET", "/api/logs/stream", nil) + q := req.URL.Query() + q.Add("id", id) + req.URL.RawQuery = q.Encode() + require.NoError(t, err, "NewRequest should not return an error.") + + mockedClient := new(MockedClient) + mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) + mockedClient.On("ContainerLogs", mock.Anything, id, "").Return(ioutil.NopCloser(strings.NewReader("")), io.EOF) + + h := handler{client: mockedClient, config: &Config{}} + handler := http.HandlerFunc(h.streamLogs) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) + mockedClient.AssertExpectations(t) +} + +func Test_handler_streamLogs_error_finding_container(t *testing.T) { + id := "123456" + req, err := http.NewRequest("GET", "/api/logs/stream", nil) + q := req.URL.Query() + q.Add("id", id) + req.URL.RawQuery = q.Encode() + require.NoError(t, err, "NewRequest should not return an error.") + + mockedClient := new(MockedClient) + mockedClient.On("FindContainer", id).Return(docker.Container{}, errors.New("error finding container")) + + h := handler{client: mockedClient, config: &Config{}} + handler := http.HandlerFunc(h.streamLogs) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) + mockedClient.AssertExpectations(t) +} + +func Test_handler_streamLogs_error_reading(t *testing.T) { + id := "123456" + req, err := http.NewRequest("GET", "/api/logs/stream", nil) + q := req.URL.Query() + q.Add("id", id) + req.URL.RawQuery = q.Encode() + require.NoError(t, err, "NewRequest should not return an error.") + + mockedClient := new(MockedClient) + mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) + mockedClient.On("ContainerLogs", mock.Anything, id, "").Return(ioutil.NopCloser(strings.NewReader("")), errors.New("test error")) + + h := handler{client: mockedClient, config: &Config{}} + handler := http.HandlerFunc(h.streamLogs) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) + mockedClient.AssertExpectations(t) +} + +func Test_handler_streamEvents_happy(t *testing.T) { + req, err := http.NewRequest("GET", "/api/events/stream", nil) + require.NoError(t, err, "NewRequest should not return an error.") + mockedClient := new(MockedClient) + messages := make(chan docker.ContainerEvent) + errChannel := make(chan error) + mockedClient.On("Events", mock.Anything).Return(messages, errChannel) + mockedClient.On("ListContainers").Return([]docker.Container{}, nil) + + go func() { + messages <- docker.ContainerEvent{ + Name: "start", + ActorID: "1234", + } + messages <- docker.ContainerEvent{ + Name: "something-random", + ActorID: "1234", + } + close(messages) + }() + + h := handler{client: mockedClient, config: &Config{}} + handler := http.HandlerFunc(h.streamEvents) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) + mockedClient.AssertExpectations(t) +} + +func Test_handler_streamEvents_error(t *testing.T) { + req, err := http.NewRequest("GET", "/api/events/stream", nil) + require.NoError(t, err, "NewRequest should not return an error.") + mockedClient := new(MockedClient) + messages := make(chan docker.ContainerEvent) + errChannel := make(chan error) + mockedClient.On("Events", mock.Anything).Return(messages, errChannel) + mockedClient.On("ListContainers").Return([]docker.Container{}, nil) + + go func() { + errChannel <- errors.New("fake error") + close(messages) + }() + + h := handler{client: mockedClient, config: &Config{}} + handler := http.HandlerFunc(h.streamEvents) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) + mockedClient.AssertExpectations(t) +} + +func Test_handler_streamEvents_error_request(t *testing.T) { + req, err := http.NewRequest("GET", "/api/events/stream", nil) + require.NoError(t, err, "NewRequest should not return an error.") + + mockedClient := new(MockedClient) + + messages := make(chan docker.ContainerEvent) + errChannel := make(chan error) + mockedClient.On("Events", mock.Anything).Return(messages, errChannel) + mockedClient.On("ListContainers").Return([]docker.Container{}, nil) + + ctx, cancel := context.WithCancel(context.Background()) + req = req.WithContext(ctx) + + go func() { + cancel() + }() + + h := handler{client: mockedClient, config: &Config{}} + handler := http.HandlerFunc(h.streamEvents) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) + mockedClient.AssertExpectations(t) +} + +// for /api/logs +func Test_handler_between_dates(t *testing.T) { + req, err := http.NewRequest("GET", "/api/logs", nil) + require.NoError(t, err, "NewRequest should not return an error.") + + from, _ := time.Parse(time.RFC3339, "2018-01-01T00:00:00Z") + to, _ := time.Parse(time.RFC3339, "2018-01-01T010:00:00Z") + + q := req.URL.Query() + q.Add("from", from.Format(time.RFC3339)) + q.Add("to", to.Format(time.RFC3339)) + q.Add("id", "123456") + req.URL.RawQuery = q.Encode() + + mockedClient := new(MockedClient) + reader := ioutil.NopCloser(strings.NewReader("2020-05-13T18:55:37.772853839Z INFO Testing logs...\n2020-05-13T18:55:37.772853839Z INFO Testing logs...\n")) + mockedClient.On("ContainerLogsBetweenDates", mock.Anything, "123456", from, to).Return(reader, nil) + + h := handler{client: mockedClient, config: &Config{}} + handler := http.HandlerFunc(h.fetchLogsBetweenDates) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) + mockedClient.AssertExpectations(t) +} diff --git a/web/routes_test.go b/web/routes_test.go index fafa036e..37a0ba63 100644 --- a/web/routes_test.go +++ b/web/routes_test.go @@ -1,27 +1,17 @@ package web import ( - "bytes" "context" - "errors" - "io" - "io/fs" - "io/ioutil" - "mime/multipart" - "net/http" - "net/http/httptest" - "os" - "strings" - "testing" "time" + "io" + "io/fs" + "github.com/gorilla/mux" - "github.com/magiconair/properties/assert" "github.com/amir20/dozzle/docker" - "github.com/beme/abide" + "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" "github.com/spf13/afero" ) @@ -64,371 +54,9 @@ func (m *MockedClient) ContainerStats(context.Context, string, chan<- docker.Con return nil } -func Test_handler_streamLogs_happy(t *testing.T) { - id := "123456" - req, err := http.NewRequest("GET", "/api/logs/stream", nil) - q := req.URL.Query() - q.Add("id", id) - req.URL.RawQuery = q.Encode() - require.NoError(t, err, "NewRequest should not return an error.") - - mockedClient := new(MockedClient) - reader := ioutil.NopCloser(strings.NewReader("INFO Testing logs...")) - mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) - mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "").Return(reader, nil) - - h := handler{client: mockedClient, config: &Config{}} - handler := http.HandlerFunc(h.streamLogs) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) - mockedClient.AssertExpectations(t) -} - -func Test_handler_streamLogs_happy_with_id(t *testing.T) { - id := "123456" - req, err := http.NewRequest("GET", "/api/logs/stream", nil) - q := req.URL.Query() - q.Add("id", id) - req.URL.RawQuery = q.Encode() - require.NoError(t, err, "NewRequest should not return an error.") - - mockedClient := new(MockedClient) - reader := ioutil.NopCloser(strings.NewReader("2020-05-13T18:55:37.772853839Z INFO Testing logs...")) - mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) - mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "").Return(reader, nil) - - h := handler{client: mockedClient, config: &Config{}} - handler := http.HandlerFunc(h.streamLogs) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) - mockedClient.AssertExpectations(t) -} - -func Test_handler_streamLogs_happy_container_stopped(t *testing.T) { - id := "123456" - req, err := http.NewRequest("GET", "/api/logs/stream", nil) - q := req.URL.Query() - q.Add("id", id) - req.URL.RawQuery = q.Encode() - require.NoError(t, err, "NewRequest should not return an error.") - - mockedClient := new(MockedClient) - mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) - mockedClient.On("ContainerLogs", mock.Anything, id, "").Return(ioutil.NopCloser(strings.NewReader("")), io.EOF) - - h := handler{client: mockedClient, config: &Config{}} - handler := http.HandlerFunc(h.streamLogs) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) - mockedClient.AssertExpectations(t) -} - -func Test_handler_streamLogs_error_finding_container(t *testing.T) { - id := "123456" - req, err := http.NewRequest("GET", "/api/logs/stream", nil) - q := req.URL.Query() - q.Add("id", id) - req.URL.RawQuery = q.Encode() - require.NoError(t, err, "NewRequest should not return an error.") - - mockedClient := new(MockedClient) - mockedClient.On("FindContainer", id).Return(docker.Container{}, errors.New("error finding container")) - - h := handler{client: mockedClient, config: &Config{}} - handler := http.HandlerFunc(h.streamLogs) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) - mockedClient.AssertExpectations(t) -} - -func Test_handler_streamLogs_error_reading(t *testing.T) { - id := "123456" - req, err := http.NewRequest("GET", "/api/logs/stream", nil) - q := req.URL.Query() - q.Add("id", id) - req.URL.RawQuery = q.Encode() - require.NoError(t, err, "NewRequest should not return an error.") - - mockedClient := new(MockedClient) - mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) - mockedClient.On("ContainerLogs", mock.Anything, id, "").Return(ioutil.NopCloser(strings.NewReader("")), errors.New("test error")) - - h := handler{client: mockedClient, config: &Config{}} - handler := http.HandlerFunc(h.streamLogs) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) - mockedClient.AssertExpectations(t) -} - -func Test_handler_streamEvents_happy(t *testing.T) { - req, err := http.NewRequest("GET", "/api/events/stream", nil) - require.NoError(t, err, "NewRequest should not return an error.") - mockedClient := new(MockedClient) - messages := make(chan docker.ContainerEvent) - errChannel := make(chan error) - mockedClient.On("Events", mock.Anything).Return(messages, errChannel) - mockedClient.On("ListContainers").Return([]docker.Container{}, nil) - - go func() { - messages <- docker.ContainerEvent{ - Name: "start", - ActorID: "1234", - } - messages <- docker.ContainerEvent{ - Name: "something-random", - ActorID: "1234", - } - close(messages) - }() - - h := handler{client: mockedClient, config: &Config{}} - handler := http.HandlerFunc(h.streamEvents) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) - mockedClient.AssertExpectations(t) -} - -func Test_handler_streamEvents_error(t *testing.T) { - req, err := http.NewRequest("GET", "/api/events/stream", nil) - require.NoError(t, err, "NewRequest should not return an error.") - mockedClient := new(MockedClient) - messages := make(chan docker.ContainerEvent) - errChannel := make(chan error) - mockedClient.On("Events", mock.Anything).Return(messages, errChannel) - mockedClient.On("ListContainers").Return([]docker.Container{}, nil) - - go func() { - errChannel <- errors.New("fake error") - close(messages) - }() - - h := handler{client: mockedClient, config: &Config{}} - handler := http.HandlerFunc(h.streamEvents) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) - mockedClient.AssertExpectations(t) -} - -func Test_handler_streamEvents_error_request(t *testing.T) { - req, err := http.NewRequest("GET", "/api/events/stream", nil) - require.NoError(t, err, "NewRequest should not return an error.") - - mockedClient := new(MockedClient) - - messages := make(chan docker.ContainerEvent) - errChannel := make(chan error) - mockedClient.On("Events", mock.Anything).Return(messages, errChannel) - mockedClient.On("ListContainers").Return([]docker.Container{}, nil) - - ctx, cancel := context.WithCancel(context.Background()) - req = req.WithContext(ctx) - - go func() { - cancel() - }() - - h := handler{client: mockedClient, config: &Config{}} - handler := http.HandlerFunc(h.streamEvents) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) - mockedClient.AssertExpectations(t) -} - -func Test_createRoutes_index(t *testing.T) { - fs := afero.NewMemMapFs() - require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.") - handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/"}) - req, err := http.NewRequest("GET", "/", nil) - require.NoError(t, err, "NewRequest should not return an error.") - rr := httptest.NewRecorder() - - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) -} - -func Test_createRoutes_redirect(t *testing.T) { - fs := afero.NewMemMapFs() - require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.") - - handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar"}) - req, err := http.NewRequest("GET", "/foobar", nil) - require.NoError(t, err, "NewRequest should not return an error.") - rr := httptest.NewRecorder() - - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) -} - -func Test_createRoutes_redirect_with_auth(t *testing.T) { - fs := afero.NewMemMapFs() - require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.") - - handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar", Username: "amir", Password: "password"}) - req, err := http.NewRequest("GET", "/foobar/", nil) - require.NoError(t, err, "NewRequest should not return an error.") - rr := httptest.NewRecorder() - - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) -} - -func Test_createRoutes_foobar(t *testing.T) { - fs := afero.NewMemMapFs() - require.NoError(t, afero.WriteFile(fs, "index.html", []byte("foo page"), 0644), "WriteFile should have no error.") - handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar"}) - req, err := http.NewRequest("GET", "/foobar/", nil) - require.NoError(t, err, "NewRequest should not return an error.") - rr := httptest.NewRecorder() - - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) -} - -func Test_createRoutes_foobar_file(t *testing.T) { - fs := afero.NewMemMapFs() - require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.") - require.NoError(t, afero.WriteFile(fs, "test", []byte("test page"), 0644), "WriteFile should have no error.") - - handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar"}) - req, err := http.NewRequest("GET", "/foobar/test", nil) - require.NoError(t, err, "NewRequest should not return an error.") - rr := httptest.NewRecorder() - - handler.ServeHTTP(rr, req) - assert.Equal(t, rr.Body.String(), "test page", "page doesn't match") -} - -func Test_createRoutes_version(t *testing.T) { - fs := afero.NewMemMapFs() - require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.") - handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/", Version: "dev"}) - req, err := http.NewRequest("GET", "/version", nil) - require.NoError(t, err, "NewRequest should not return an error.") - rr := httptest.NewRecorder() - - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) -} - -func Test_createRoutes_username_password(t *testing.T) { - - handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"}) - req, err := http.NewRequest("GET", "/", nil) - require.NoError(t, err, "NewRequest should not return an error.") - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) -} - -func Test_createRoutes_username_password_invalid(t *testing.T) { - handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"}) - req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil) - require.NoError(t, err, "NewRequest should not return an error.") - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) -} - -func Test_createRoutes_username_password_login_happy(t *testing.T) { - handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"}) - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - fw, err := writer.CreateFormField("username") - require.NoError(t, err, "Creating field should not be error.") - _, err = io.Copy(fw, strings.NewReader("amir")) - require.NoError(t, err, "Copying field should not result in error.") - - fw, err = writer.CreateFormField("password") - require.NoError(t, err, "Creating field should not be error.") - _, err = io.Copy(fw, strings.NewReader("password")) - require.NoError(t, err, "Copying field should not result in error.") - - writer.Close() - - req, err := http.NewRequest("POST", "/api/validateCredentials", bytes.NewReader(body.Bytes())) - req.Header.Set("Content-Type", writer.FormDataContentType()) - - require.NoError(t, err, "NewRequest should not return an error.") - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - assert.Equal(t, rr.Code, 200) - cookie := rr.Header().Get("Set-Cookie") - assert.Matches(t, cookie, "session=.+") -} - -func Test_createRoutes_username_password_login_failed(t *testing.T) { - handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"}) - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - fw, err := writer.CreateFormField("username") - require.NoError(t, err, "Creating field should not be error.") - _, err = io.Copy(fw, strings.NewReader("amir")) - require.NoError(t, err, "Copying field should not result in error.") - - fw, err = writer.CreateFormField("password") - require.NoError(t, err, "Creating field should not be error.") - _, err = io.Copy(fw, strings.NewReader("bad")) - require.NoError(t, err, "Copying field should not result in error.") - - writer.Close() - - req, err := http.NewRequest("POST", "/api/validateCredentials", bytes.NewReader(body.Bytes())) - req.Header.Set("Content-Type", writer.FormDataContentType()) - - require.NoError(t, err, "NewRequest should not return an error.") - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - assert.Equal(t, rr.Code, 401) -} - -func Test_createRoutes_username_password_valid_session(t *testing.T) { - mockedClient := new(MockedClient) - mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil) - mockedClient.On("ContainerLogs", mock.Anything, "123", "").Return(ioutil.NopCloser(strings.NewReader("test data")), io.EOF) - handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"}) - - // Get cookie first - req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil) - require.NoError(t, err, "NewRequest should not return an error.") - session, _ := store.Get(req, sessionName) - session.Values[authorityKey] = time.Now().Unix() - recorder := httptest.NewRecorder() - session.Save(req, recorder) - cookies := recorder.Result().Cookies() - - // Test with cookie - req, err = http.NewRequest("GET", "/api/logs/stream?id=123", nil) - require.NoError(t, err, "NewRequest should not return an error.") - req.AddCookie(cookies[0]) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - abide.AssertHTTPResponse(t, t.Name(), rr.Result()) -} - -func Test_createRoutes_username_password_invalid_session(t *testing.T) { - mockedClient := new(MockedClient) - mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil) - mockedClient.On("ContainerLogs", mock.Anything, "since").Return(ioutil.NopCloser(strings.NewReader("test data")), io.EOF) - handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"}) - req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil) - require.NoError(t, err, "NewRequest should not return an error.") - req.AddCookie(&http.Cookie{Name: "session", Value: "baddata"}) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - assert.Equal(t, rr.Code, 401) +func (m *MockedClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time) (io.ReadCloser, error) { + args := m.Called(ctx, id, from, to) + return args.Get(0).(io.ReadCloser), args.Error(1) } func createHandler(client docker.Client, content fs.FS, config Config) *mux.Router { @@ -449,9 +77,3 @@ func createHandler(client docker.Client, content fs.FS, config Config) *mux.Rout config: &config, }) } - -func TestMain(m *testing.M) { - exit := m.Run() - abide.Cleanup() - os.Exit(exit) -}