go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/appengine/coordinator/flex/logs/http_test.go (about)

     1  // Copyright 2018 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package logs
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"html/template"
    21  	"net/http/httptest"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"go.chromium.org/luci/common/clock/testclock"
    27  	"go.chromium.org/luci/common/errors"
    28  
    29  	"go.chromium.org/luci/logdog/api/logpb"
    30  	ct "go.chromium.org/luci/logdog/appengine/coordinator/coordinatorTest"
    31  
    32  	. "github.com/smartystreets/goconvey/convey"
    33  )
    34  
    35  func TestHTTP(t *testing.T) {
    36  	t.Parallel()
    37  
    38  	Convey(`With a testing configuration`, t, func() {
    39  		c, env := ct.Install()
    40  		c, tc := testclock.UseTime(c, testclock.TestRecentTimeUTC)
    41  
    42  		const project = "proj-foo"
    43  		const realm = "some-realm"
    44  
    45  		env.AddProject(c, project)
    46  		env.ActAsReader(project, realm)
    47  
    48  		tls := ct.MakeStream(c, project, realm, "testing/+/foo/bar")
    49  		So(tls.Put(c), ShouldBeNil)
    50  
    51  		resp := httptest.NewRecorder()
    52  		var options userOptions
    53  
    54  		fakeData := func(data []logResp) logData {
    55  			ch := make(chan logResp)
    56  			result := logData{
    57  				ch:      ch,
    58  				options: options,
    59  				logDesc: tls.Desc,
    60  			}
    61  			go func() {
    62  				defer close(ch)
    63  				for _, item := range data {
    64  					ch <- item
    65  				}
    66  			}()
    67  			return result
    68  		}
    69  
    70  		Convey(`Do nothing`, func() {
    71  			data := fakeData([]logResp{})
    72  			So(serve(c, data, resp), ShouldBeNil)
    73  		})
    74  
    75  		Convey(`Single Log raw`, func() {
    76  			options.format = "raw"
    77  			data := fakeData([]logResp{
    78  				{desc: tls.Desc, log: tls.LogEntry(c, 0)},
    79  			})
    80  			So(serve(c, data, resp), ShouldBeNil)
    81  			So(resp.Body.String(), ShouldResemble, "log entry #0\n")
    82  			// Note: It's not helpful to assert HTTP header values here,
    83  			// because this tests serve(), which doesn't set HTTP headers.
    84  		})
    85  
    86  		Convey(`Single Log full HTML`, func() {
    87  			options.format = "full"
    88  			l1 := tls.LogEntry(c, 0)
    89  			tc.Add(time.Minute)
    90  			l2 := tls.LogEntry(c, 1)
    91  			data := fakeData([]logResp{
    92  				{desc: tls.Desc, log: l1},
    93  				{desc: tls.Desc, log: l2},
    94  			})
    95  			So(serve(c, data, resp), ShouldBeNil)
    96  			body := resp.Body.String()
    97  			body = strings.Replace(body, "\n", "", -1)
    98  			body = strings.Replace(body, "\t", "", -1)
    99  			So(body, ShouldContainSubstring, `<div class="line" id="L0_0">`)
   100  			So(body, ShouldContainSubstring, `data-timestamp="1454472306000"`)
   101  			So(body, ShouldContainSubstring, `log entry #1`)
   102  		})
   103  
   104  		Convey(`Single error, full HTML`, func() {
   105  			options.format = "full"
   106  			msg := "error encountered <script>alert()</script>"
   107  			data := fakeData([]logResp{
   108  				{err: errors.New(msg)},
   109  			})
   110  			So(serve(c, data, resp).Error(), ShouldResemble, msg)
   111  			body := resp.Body.String()
   112  			body = strings.Replace(body, "\n", "", -1)
   113  			body = strings.Replace(body, "\t", "", -1)
   114  			// Note: HTML escapes don't show up in the GoConvey web interface.
   115  			So(body, ShouldEqual, fmt.Sprintf(`<div class="error line">LOGDOG ERROR: %s</div>`, template.HTMLEscapeString(msg)))
   116  		})
   117  	})
   118  
   119  }
   120  
   121  func TestHelperFunctions(t *testing.T) {
   122  	t.Parallel()
   123  
   124  	Convey(`linkify changes URLs to HTML links`, t, func() {
   125  		Convey(`with nothing that looks like a URL`, func() {
   126  			So(linkify(""), ShouldEqual, template.HTML(""))
   127  			So(linkify(" foo "), ShouldEqual, template.HTML(" foo "))
   128  		})
   129  
   130  		Convey(`does normal HTML escaping`, func() {
   131  			So(linkify("<foo>"), ShouldEqual, template.HTML("&lt;foo&gt;"))
   132  		})
   133  
   134  		Convey(`with invalid URLs`, func() {
   135  			So(linkify("example.com"), ShouldEqual, template.HTML("example.com"))
   136  			So(linkify("http: //example.com"), ShouldEqual, template.HTML("http: //example.com"))
   137  			So(linkify("ftp://example.com/foo"), ShouldEqual, template.HTML("ftp://example.com/foo"))
   138  			So(linkify("xhttp://example.com/"), ShouldEqual, template.HTML("xhttp://example.com/"))
   139  			So(linkify("http://ex~ample.com"), ShouldEqual, template.HTML("http://ex~ample.com"))
   140  		})
   141  
   142  		Convey(`with single simple URLs`, func() {
   143  			So(linkify("http://example.com"), ShouldEqual,
   144  				template.HTML(`<a href="http://example.com">http://example.com</a>`))
   145  			So(linkify("https://example2.com"), ShouldEqual,
   146  				template.HTML(`<a href="https://example2.com">https://example2.com</a>`))
   147  			So(linkify("https://example.com/$foo"), ShouldEqual,
   148  				template.HTML(`<a href="https://example.com/$foo">https://example.com/$foo</a>`))
   149  			So(linkify("https://example.com/5%20%22/#x x"), ShouldEqual,
   150  				template.HTML(`<a href="https://example.com/5%20%22/#x">https://example.com/5%20%22/#x</a> x`))
   151  			So(linkify("https://example.com.:443"), ShouldEqual,
   152  				template.HTML(`<a href="https://example.com.:443">https://example.com.:443</a>`))
   153  		})
   154  
   155  		Convey(`with single URLs that have " and & in the path part`, func() {
   156  			So(linkify("https://example.com/\"/x"), ShouldEqual,
   157  				template.HTML(`<a href="https://example.com/%22/x">https://example.com/&#34;/x</a>`))
   158  			So(linkify("https://example.com/&/x"), ShouldEqual,
   159  				template.HTML(`<a href="https://example.com/&amp;/x">https://example.com/&amp;/x</a>`))
   160  		})
   161  
   162  		Convey(`with single URLs that have " and <> immediately after the domain`, func() {
   163  			So(linkify(`https://example.com"lol</a>CRAFTED_HTML`), ShouldEqual,
   164  				template.HTML(`<a href="https://example.com">https://example.com</a>&#34;lol&lt;/a&gt;CRAFTED_HTML`))
   165  		})
   166  
   167  		Convey(`with single URLs that have " and <> in the path part`, func() {
   168  			So(linkify(`https://example.com/"lol</a>CRAFTED_HTML`), ShouldEqual,
   169  				template.HTML(`<a href="https://example.com/%22lol%3c/a%3eCRAFTED_HTML">https://example.com/&#34;lol&lt;/a&gt;CRAFTED_HTML</a>`))
   170  		})
   171  
   172  		Convey(`with multiple URLs`, func() {
   173  			So(linkify("x http://x.com z http://y.com y"), ShouldEqual,
   174  				template.HTML(`x <a href="http://x.com">http://x.com</a> z <a href="http://y.com">http://y.com</a> y`))
   175  			So(linkify("http://x.com http://y.com"), ShouldEqual,
   176  				template.HTML(`<a href="http://x.com">http://x.com</a> <a href="http://y.com">http://y.com</a>`))
   177  		})
   178  
   179  		Convey(`lineTemplate uses linkify`, func() {
   180  			lt := logLineStruct{Text: "See https://crbug.com/1167332."}
   181  			w := bytes.NewBuffer([]byte{})
   182  			So(lineTemplate.Execute(w, lt), ShouldBeNil)
   183  			So(w.String(), ShouldContainSubstring,
   184  				`<span class="text">See <a href="https://crbug.com/1167332">https://crbug.com/1167332</a>.</span>`)
   185  		})
   186  
   187  	})
   188  
   189  	Convey(`contentTypeHeader adjusts based on format and data content type`, t, func() {
   190  		// Using incomplete logData as the headers only depend on metadata, not
   191  		// actual log responses. This is a small unit test only testing one
   192  		// small behavior which is not covered by the larger serve() tests
   193  		// above.
   194  
   195  		Convey(`Raw text with default text encoding`, func() {
   196  			So(contentTypeHeader(logData{
   197  				options: userOptions{format: "raw"},
   198  				logDesc: &logpb.LogStreamDescriptor{ContentType: "text/plain; charset=utf-8"},
   199  			}), ShouldEqual, "text/plain; charset=utf-8")
   200  		})
   201  
   202  		Convey(`Raw text with alternate text encoding`, func() {
   203  			So(contentTypeHeader(logData{
   204  				options: userOptions{format: "raw"},
   205  				logDesc: &logpb.LogStreamDescriptor{ContentType: "text/plain; charset=iso-unicorns"},
   206  			}), ShouldEqual, "text/plain; charset=iso-unicorns")
   207  		})
   208  
   209  		Convey(`HTML with default text encoding`, func() {
   210  			So(contentTypeHeader(logData{
   211  				options: userOptions{format: "full"},
   212  				logDesc: &logpb.LogStreamDescriptor{ContentType: "text/plain; charset=utf-8"},
   213  			}), ShouldEqual, "text/html; charset=utf-8")
   214  		})
   215  
   216  		Convey(`HTML with alternate text encoding`, func() {
   217  			So(contentTypeHeader(logData{
   218  				options: userOptions{format: "full"},
   219  				logDesc: &logpb.LogStreamDescriptor{ContentType: "text/plain; charset=iso-unicorns"},
   220  			}), ShouldEqual, "text/html; charset=iso-unicorns")
   221  		})
   222  	})
   223  }