github.com/blend/go-sdk@v1.20220411.3/web/app_test.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package web
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	"fmt"
    14  	"net/http"
    15  	"strings"
    16  	"sync"
    17  	"testing"
    18  	"time"
    19  
    20  	"github.com/blend/go-sdk/assert"
    21  	"github.com/blend/go-sdk/env"
    22  	"github.com/blend/go-sdk/ex"
    23  	"github.com/blend/go-sdk/graceful"
    24  	"github.com/blend/go-sdk/logger"
    25  	"github.com/blend/go-sdk/webutil"
    26  )
    27  
    28  // assert an app is graceful
    29  var (
    30  	_ graceful.Graceful = (*App)(nil)
    31  )
    32  
    33  func controllerNoOp(_ *Ctx) Result { return nil }
    34  
    35  type testController struct {
    36  	callback func(app *App)
    37  }
    38  
    39  func (tc testController) Register(app *App) {
    40  	if tc.callback != nil {
    41  		tc.callback(app)
    42  	}
    43  }
    44  
    45  func TestAppNew(t *testing.T) {
    46  	assert := assert.New(t)
    47  
    48  	app, err := New()
    49  	assert.Nil(err)
    50  	assert.NotNil(app.BaseState)
    51  	assert.NotNil(app.Views)
    52  	assert.Equal(http.SameSiteLaxMode, app.Auth.CookieDefaults.SameSite)
    53  }
    54  
    55  func TestAppNewFromConfig(t *testing.T) {
    56  	assert := assert.New(t)
    57  
    58  	app, err := New(OptConfig(Config{
    59  		BindAddr:               ":5555",
    60  		Port:                   5000,
    61  		HandleMethodNotAllowed: true,
    62  		HandleOptions:          true,
    63  		DisablePanicRecovery:   true,
    64  
    65  		MaxHeaderBytes:    128,
    66  		ReadHeaderTimeout: 5 * time.Second,
    67  		ReadTimeout:       6 * time.Second,
    68  		IdleTimeout:       7 * time.Second,
    69  		WriteTimeout:      8 * time.Second,
    70  
    71  		CookieName: "A GOOD ONE",
    72  		Views: ViewCacheConfig{
    73  			LiveReload: true,
    74  		},
    75  	}))
    76  
    77  	assert.Nil(err)
    78  
    79  	assert.Equal(":5555", app.Config.BindAddr)
    80  	assert.True(app.Config.HandleMethodNotAllowed)
    81  	assert.True(app.Config.HandleOptions)
    82  	assert.True(app.Config.DisablePanicRecovery)
    83  	assert.Equal(128, app.Config.MaxHeaderBytes)
    84  	assert.Equal(5*time.Second, app.Config.ReadHeaderTimeout)
    85  	assert.Equal(6*time.Second, app.Config.ReadTimeout)
    86  	assert.Equal(7*time.Second, app.Config.IdleTimeout)
    87  	assert.Equal(8*time.Second, app.Config.WriteTimeout)
    88  	assert.Equal("A GOOD ONE", app.Auth.CookieDefaults.Name, "we should use the auth config for the auth manager")
    89  	assert.True(app.Views.LiveReload, "we should use the view cache config for the view cache")
    90  }
    91  
    92  func TestAppRegister(t *testing.T) {
    93  	assert := assert.New(t)
    94  	called := false
    95  	c := &testController{
    96  		callback: func(_ *App) {
    97  			called = true
    98  		},
    99  	}
   100  	app, err := New()
   101  
   102  	assert.Nil(err)
   103  	assert.False(called)
   104  	app.Register(c)
   105  	assert.True(called)
   106  }
   107  
   108  func TestAppPathParams(t *testing.T) {
   109  	assert := assert.New(t)
   110  
   111  	var route *Route
   112  	var params RouteParameters
   113  	app, err := New()
   114  	assert.Nil(err)
   115  	app.GET("/:uuid", func(c *Ctx) Result {
   116  		route = c.Route
   117  		params = c.RouteParams
   118  		return Raw([]byte("ok!"))
   119  	})
   120  
   121  	route, params, skipSlashRedirect := app.Lookup("GET", "/foo")
   122  	assert.NotNil(route)
   123  	assert.NotEmpty(params)
   124  	assert.Equal("foo", params.Get("uuid"))
   125  	assert.False(skipSlashRedirect)
   126  
   127  	meta, err := MockGet(app, "/foo").Discard()
   128  	assert.Nil(err, fmt.Sprintf("%+v", err))
   129  	assert.Equal(http.StatusOK, meta.StatusCode)
   130  	assert.NotNil(route)
   131  	assert.Equal("GET", route.Method)
   132  	assert.Equal("/:uuid", route.Path)
   133  	assert.NotNil(route.Handler)
   134  
   135  	assert.NotEmpty(params)
   136  	assert.Equal("foo", params.Get("uuid"))
   137  }
   138  
   139  func TestAppPathParamsForked(t *testing.T) {
   140  	/*
   141  		this test should assert that we can have a common structure of routes
   142  		namely that you can have some shared prefix but differentiate by plural.
   143  	*/
   144  
   145  	assert := assert.New(t)
   146  
   147  	var route *Route
   148  	var params RouteParameters
   149  	app, err := New()
   150  	assert.Nil(err)
   151  	app.GET("/foo/:uuid", func(c *Ctx) Result { return NoContent })
   152  	app.GET("/foos/bar/:uuid", func(c *Ctx) Result {
   153  		route = c.Route
   154  		params = c.RouteParams
   155  		return Raw([]byte("ok!"))
   156  	})
   157  
   158  	meta, err := MockGet(app, "/foos/bar/foo").Discard()
   159  	assert.Nil(err)
   160  	assert.Equal(http.StatusOK, meta.StatusCode)
   161  	assert.NotNil(route)
   162  	assert.Equal("GET", route.Method)
   163  	assert.Equal("/foos/bar/:uuid", route.Path)
   164  	assert.NotNil(route.Handler)
   165  
   166  	assert.NotNil(params)
   167  	assert.NotEmpty(params)
   168  	assert.Equal("foo", params.Get("uuid"))
   169  }
   170  
   171  func TestAppSetLogger(t *testing.T) {
   172  	assert := assert.New(t)
   173  
   174  	log := logger.MustNew()
   175  	app, err := New(OptLog(log))
   176  	assert.Nil(err)
   177  	assert.NotNil(app.Log)
   178  }
   179  
   180  func TestAppCreateStaticMountedRoute(t *testing.T) {
   181  	assert := assert.New(t)
   182  	app, err := New()
   183  	assert.Nil(err)
   184  	assert.Equal("/testPath/*filepath", app.formatStaticMountRoute("/testPath/*filepath"))
   185  	assert.Equal("/testPath/*filepath", app.formatStaticMountRoute("/testPath/"))
   186  	assert.Equal("/testPath/*filepath", app.formatStaticMountRoute("/testPath"))
   187  }
   188  
   189  func TestAppStaticRewrite(t *testing.T) {
   190  	assert := assert.New(t)
   191  	app, err := New()
   192  	assert.Nil(err)
   193  
   194  	app.ServeStatic("/testPath", []string{"_static"})
   195  	assert.NotEmpty(app.Statics)
   196  	assert.NotNil(app.Statics["/testPath/*filepath"])
   197  	assert.Nil(app.SetStaticRewriteRule("/testPath", "(.*)", func(path string, pieces ...string) string {
   198  		return path
   199  	}))
   200  	assert.NotNil(app.SetStaticRewriteRule("/notapath", "(.*)", func(path string, pieces ...string) string {
   201  		return path
   202  	}))
   203  
   204  	assert.NotEmpty(app.Statics["/testPath/*filepath"].RewriteRules)
   205  }
   206  
   207  func TestAppStaticRewriteBadExp(t *testing.T) {
   208  	assert := assert.New(t)
   209  	app, err := New()
   210  	assert.Nil(err)
   211  
   212  	app.ServeStatic("/testPath", []string{"_static"})
   213  	assert.NotEmpty(app.Statics)
   214  	assert.NotNil(app.Statics["/testPath/*filepath"])
   215  
   216  	err = app.SetStaticRewriteRule("/testPath", "((((", func(path string, pieces ...string) string {
   217  		return path
   218  	})
   219  
   220  	assert.NotNil(err)
   221  	assert.Empty(app.Statics["/testPath/*filepath"].RewriteRules)
   222  }
   223  
   224  func TestAppStaticHeader(t *testing.T) {
   225  	assert := assert.New(t)
   226  	app, err := New()
   227  	assert.Nil(err)
   228  
   229  	app.ServeStatic("/testPath", []string{"_static"})
   230  	assert.NotEmpty(app.Statics)
   231  	assert.NotNil(app.Statics["/testPath/*filepath"])
   232  	assert.Nil(app.SetStaticHeader("/testPath/*filepath", "cache-control", "haha what is caching."))
   233  	assert.NotNil(app.SetStaticHeader("/notaroute", "cache-control", "haha what is caching."))
   234  	assert.NotEmpty(app.Statics["/testPath/*filepath"].Headers)
   235  }
   236  
   237  func TestAppMiddleWarePipeline(t *testing.T) {
   238  	assert := assert.New(t)
   239  
   240  	didRun := false
   241  
   242  	app, err := New()
   243  	assert.Nil(err)
   244  
   245  	app.GET("/",
   246  		func(r *Ctx) Result { return Raw([]byte("OK!")) },
   247  		func(action Action) Action {
   248  			didRun = true
   249  			return action
   250  		},
   251  		func(action Action) Action {
   252  			return func(r *Ctx) Result {
   253  				return Raw([]byte("foo"))
   254  			}
   255  		},
   256  	)
   257  
   258  	result, _, err := MockGet(app, "/").Bytes()
   259  	assert.Nil(err)
   260  	assert.True(didRun)
   261  	assert.Equal("foo", string(result))
   262  }
   263  
   264  func TestAppStatic(t *testing.T) {
   265  	assert := assert.New(t)
   266  
   267  	app, err := New()
   268  	assert.Nil(err)
   269  
   270  	app.ServeStatic("/static/*filepath", []string{"testdata"})
   271  
   272  	index, _, err := MockGet(app, "/static/test_file.html").Bytes()
   273  	assert.Nil(err)
   274  	assert.True(strings.Contains(string(index), "Test!"), string(index))
   275  }
   276  
   277  func TestAppStaticSingleFile(t *testing.T) {
   278  	assert := assert.New(t)
   279  	app, err := New()
   280  	assert.Nil(err)
   281  
   282  	app.GET("/", func(r *Ctx) Result {
   283  		return Static("testdata/test_file.html")
   284  	})
   285  
   286  	index, _, err := MockGet(app, "/").Bytes()
   287  	assert.Nil(err)
   288  	assert.True(strings.Contains(string(index), "Test!"), string(index))
   289  }
   290  
   291  func TestAppProviderMiddleware(t *testing.T) {
   292  	assert := assert.New(t)
   293  
   294  	var okAction = func(r *Ctx) Result {
   295  		assert.NotNil(r.DefaultProvider)
   296  		return Raw([]byte("OK!"))
   297  	}
   298  
   299  	app, err := New()
   300  	assert.Nil(err)
   301  
   302  	app.GET("/", okAction, JSONProviderAsDefault)
   303  
   304  	_, err = MockGet(app, "/").Discard()
   305  	assert.Nil(err)
   306  }
   307  
   308  func TestAppProviderMiddlewareOrder(t *testing.T) {
   309  	assert := assert.New(t)
   310  
   311  	app, err := New()
   312  	assert.Nil(err)
   313  
   314  	var okAction = func(r *Ctx) Result {
   315  		assert.NotNil(r.DefaultProvider)
   316  		return Raw([]byte("OK!"))
   317  	}
   318  
   319  	var dependsOnProvider = func(action Action) Action {
   320  		return func(r *Ctx) Result {
   321  			assert.NotNil(r.DefaultProvider)
   322  			return action(r)
   323  		}
   324  	}
   325  
   326  	app.GET("/", okAction, dependsOnProvider, JSONProviderAsDefault)
   327  	_, err = MockGet(app, "/").Discard()
   328  	assert.Nil(err)
   329  }
   330  
   331  func TestAppDefaultResultProviderWithDefault(t *testing.T) {
   332  	assert := assert.New(t)
   333  
   334  	app, err := New(OptUse(ViewProviderAsDefault))
   335  	assert.Nil(err)
   336  	assert.NotEmpty(app.BaseMiddleware)
   337  
   338  	rc := NewCtx(nil, nil, app.ctxOptions(context.Background(), nil, nil)...)
   339  
   340  	// this will be set to the default initially
   341  	assert.NotNil(rc.DefaultProvider)
   342  
   343  	app.GET("/", func(ctx *Ctx) Result {
   344  		assert.NotNil(ctx.DefaultProvider)
   345  		_, isTyped := ctx.DefaultProvider.(*ViewCache)
   346  		assert.True(isTyped)
   347  		return nil
   348  	})
   349  	_, err = MockGet(app, "/").Discard()
   350  	assert.Nil(err)
   351  }
   352  
   353  func TestAppDefaultResultProviderWithDefaultFromRoute(t *testing.T) {
   354  	assert := assert.New(t)
   355  
   356  	app, err := New(OptUse(JSONProviderAsDefault))
   357  	assert.Nil(err)
   358  
   359  	app.Views.AddLiterals(DefaultTemplateNotAuthorized)
   360  	app.GET("/", controllerNoOp, SessionRequired, ViewProviderAsDefault)
   361  
   362  	//somehow assert that the content type is html
   363  	meta, err := MockGet(app, "/").Discard()
   364  	assert.Nil(err)
   365  	defer meta.Body.Close()
   366  
   367  	assert.Equal(webutil.ContentTypeHTML, meta.Header.Get(webutil.HeaderContentType))
   368  }
   369  
   370  func TestAppViewResult(t *testing.T) {
   371  	assert := assert.New(t)
   372  
   373  	app, err := New()
   374  	assert.Nil(err)
   375  
   376  	app.Views.AddPaths("testdata/test_file.html")
   377  	app.GET("/", func(r *Ctx) Result {
   378  		return r.Views.View("test", "foobarbaz")
   379  	})
   380  
   381  	contents, meta, err := MockGet(app, "/").Bytes()
   382  	assert.Nil(err)
   383  	assert.Equal(http.StatusOK, meta.StatusCode, string(contents))
   384  	assert.Equal(webutil.ContentTypeHTML, meta.Header.Get(webutil.HeaderContentType))
   385  	assert.Contains(string(contents), "foobarbaz")
   386  }
   387  
   388  func TestAppWritesLogsByDefault(t *testing.T) {
   389  	assert := assert.New(t)
   390  
   391  	buffer := bytes.NewBuffer(nil)
   392  	agent := logger.MustNew(
   393  		logger.OptAll(),
   394  		logger.OptFormatter(
   395  			logger.NewTextOutputFormatter(
   396  				logger.OptTextHideTimestamp(),
   397  				logger.OptTextNoColor(),
   398  			),
   399  		),
   400  		logger.OptOutput(buffer),
   401  	)
   402  
   403  	app, err := New(
   404  		OptLog(agent),
   405  	)
   406  	assert.Nil(err)
   407  
   408  	app.GET("/", func(r *Ctx) Result {
   409  		return Raw([]byte("ok!"))
   410  	})
   411  	_, err = MockGet(app, "/").Discard()
   412  	assert.Nil(err)
   413  	agent.Drain()
   414  	assert.NotZero(buffer.Len())
   415  	assert.NotEmpty(buffer.String())
   416  
   417  	assert.Matches(`\[http.request\] 127\.0\.0\.1 GET \/ 200 (.*) text\/plain; charset=utf-8 3B	web.route=\/\n`, buffer.String(), "buffer should contain the non-zero status code") // we use a prefix here because the elapsed time is variable.
   418  }
   419  
   420  func TestAppBindAddr(t *testing.T) {
   421  	assert := assert.New(t)
   422  
   423  	env.Env().Set("BIND_ADDR", ":9999")
   424  	env.Env().Set("PORT", "1111")
   425  	defer env.Restore()
   426  
   427  	assert.Equal(":3333", MustNew(OptBindAddr(":3333")).Config.BindAddr)
   428  	assert.Equal(":2222", MustNew(OptPort(2222)).Config.BindAddr)
   429  }
   430  
   431  func TestAppNotFound(t *testing.T) {
   432  	assert := assert.New(t)
   433  
   434  	app, err := New()
   435  	assert.Nil(err)
   436  
   437  	app.GET("/", func(r *Ctx) Result {
   438  		return Raw([]byte("ok!"))
   439  	})
   440  
   441  	wg := sync.WaitGroup{}
   442  	wg.Add(1)
   443  
   444  	app.NotFoundHandler = app.RenderAction(func(r *Ctx) Result {
   445  		defer wg.Done()
   446  		return JSON.NotFound()
   447  	})
   448  	_, err = MockGet(app, "/doesntexist").Discard()
   449  	assert.Nil(err)
   450  	wg.Wait()
   451  }
   452  
   453  func TestAppDefaultHeaders(t *testing.T) {
   454  	assert := assert.New(t)
   455  	app, err := New(OptDefaultHeader("foo", "bar"), OptDefaultHeader("baz", "buzz"))
   456  	assert.Nil(err)
   457  	assert.Equal([]string{"buzz"}, app.BaseHeaders[http.CanonicalHeaderKey("baz")])
   458  
   459  	app.GET("/", func(r *Ctx) Result {
   460  		return Text.Result("ok")
   461  	})
   462  
   463  	meta, err := MockGet(app, "/").Discard()
   464  	assert.Nil(err)
   465  	assert.NotEmpty(meta.Header)
   466  	assert.Equal("bar", meta.Header.Get("foo"))
   467  	assert.Equal("buzz", meta.Header.Get("baz"))
   468  }
   469  
   470  func TestAppViewErrorsRenderErrorView(t *testing.T) {
   471  	assert := assert.New(t)
   472  
   473  	app, err := New()
   474  	assert.Nil(err)
   475  	assert.NotNil(app.Views)
   476  
   477  	app.Views.AddLiterals(`{{ define "malformed" }} {{ .Ctx ALSKADJALSKDJA }} {{ end }}`)
   478  	app.GET("/", func(r *Ctx) Result {
   479  		return r.Views.View("malformed", nil)
   480  	})
   481  	_, err = MockGet(app, "/").Discard()
   482  	assert.NotNil(err)
   483  }
   484  
   485  func TestAppAddsDefaultHeaders(t *testing.T) {
   486  	assert := assert.New(t)
   487  
   488  	app, err := New(OptBindAddr(DefaultMockBindAddr))
   489  	assert.Nil(err)
   490  
   491  	app.GET("/", func(r *Ctx) Result {
   492  		return Text.Result("OK!")
   493  	})
   494  
   495  	go func() { _ = app.Start() }()
   496  	<-app.NotifyStarted()
   497  	defer func() { _ = app.Stop() }()
   498  
   499  	res, err := http.Get("http://" + app.Listener.Addr().String() + "/")
   500  	assert.Nil(err)
   501  	assert.NotEmpty(res.Header)
   502  	assert.Equal(PackageName, res.Header.Get(webutil.HeaderServer))
   503  }
   504  
   505  func TestAppHandlesPanics(t *testing.T) {
   506  	assert := assert.New(t)
   507  
   508  	app, err := New(OptBindAddr(DefaultMockBindAddr))
   509  	assert.Nil(err)
   510  
   511  	app.GET("/", func(r *Ctx) Result {
   512  		panic("this is only a test")
   513  	})
   514  
   515  	var didRecover bool
   516  	go func() {
   517  		defer func() {
   518  			if r := recover(); r != nil {
   519  				didRecover = true
   520  			}
   521  		}()
   522  		_ = app.Start()
   523  	}()
   524  	defer func() { _ = app.Stop() }()
   525  	<-app.NotifyStarted()
   526  
   527  	res, err := http.Get("http://" + app.Listener.Addr().String() + "/")
   528  	assert.Nil(err)
   529  	assert.Equal(http.StatusInternalServerError, res.StatusCode)
   530  	assert.False(didRecover)
   531  }
   532  
   533  var (
   534  	_ Tracer     = (*mockTracer)(nil)
   535  	_ ViewTracer = (*mockTracer)(nil)
   536  )
   537  
   538  type mockTracer struct {
   539  	OnStart  func(*Ctx)
   540  	OnFinish func(*Ctx, error)
   541  
   542  	OnViewStart  func(*Ctx, *ViewResult)
   543  	OnViewFinish func(*Ctx, *ViewResult, error)
   544  }
   545  
   546  func (mt mockTracer) Start(ctx *Ctx) TraceFinisher {
   547  	if mt.OnStart != nil {
   548  		mt.OnStart(ctx)
   549  	}
   550  	return &mockTraceFinisher{parent: &mt}
   551  }
   552  
   553  func (mt mockTracer) StartView(ctx *Ctx, vr *ViewResult) ViewTraceFinisher {
   554  	if mt.OnViewStart != nil {
   555  		mt.OnViewStart(ctx, vr)
   556  	}
   557  	return &mockViewTraceFinisher{parent: &mt}
   558  }
   559  
   560  type mockTraceFinisher struct {
   561  	parent *mockTracer
   562  }
   563  
   564  func (mtf mockTraceFinisher) Finish(ctx *Ctx, err error) {
   565  	mtf.parent.OnFinish(ctx, err)
   566  }
   567  
   568  type mockViewTraceFinisher struct {
   569  	parent *mockTracer
   570  }
   571  
   572  func (mvf mockViewTraceFinisher) FinishView(ctx *Ctx, vr *ViewResult, err error) {
   573  	mvf.parent.OnViewFinish(ctx, vr, err)
   574  }
   575  
   576  func ok(_ *Ctx) Result            { return JSON.OK() }
   577  func doPanic(_ *Ctx) Result       { panic("this is only a test") }
   578  func internalError(_ *Ctx) Result { return JSON.InternalError(fmt.Errorf("only a test")) }
   579  func viewOK(ctx *Ctx) Result      { return ctx.Views.View("ok", nil) }
   580  
   581  func TestAppTracer(t *testing.T) {
   582  	assert := assert.New(t)
   583  
   584  	wg := sync.WaitGroup{}
   585  	wg.Add(2)
   586  
   587  	var hasValue bool
   588  
   589  	app, err := New()
   590  	assert.Nil(err)
   591  
   592  	app.GET("/", ok)
   593  	app.Tracer = mockTracer{
   594  		OnStart: func(ctx *Ctx) {
   595  			defer wg.Done()
   596  			ctx.WithStateValue("foo", "bar")
   597  		},
   598  		OnFinish: func(ctx *Ctx, err error) {
   599  			defer wg.Done()
   600  			hasValue = ctx.StateValue("foo") != nil
   601  		},
   602  	}
   603  
   604  	_, err = MockGet(app, "/").Discard()
   605  	assert.Nil(err)
   606  	wg.Wait()
   607  
   608  	assert.True(hasValue)
   609  }
   610  
   611  func TestAppTracerError(t *testing.T) {
   612  	assert := assert.New(t)
   613  
   614  	wg := sync.WaitGroup{}
   615  	wg.Add(1)
   616  
   617  	var hasError bool
   618  
   619  	app, err := New()
   620  	assert.Nil(err)
   621  
   622  	app.GET("/", ok)
   623  	app.GET("/error", internalError)
   624  	app.Tracer = mockTracer{
   625  		OnFinish: func(ctx *Ctx, err error) {
   626  			defer wg.Done()
   627  			hasError = err != nil
   628  		},
   629  	}
   630  
   631  	_, err = MockGet(app, "/error").Discard()
   632  	assert.Nil(err)
   633  	wg.Wait()
   634  	assert.True(hasError)
   635  }
   636  
   637  func TestAppViewTracer(t *testing.T) {
   638  	assert := assert.New(t)
   639  
   640  	wg := sync.WaitGroup{}
   641  	wg.Add(4)
   642  
   643  	var hasValue bool
   644  
   645  	app, err := New()
   646  	assert.Nil(err)
   647  
   648  	app.Views.AddLiterals("{{ define \"ok\" }}ok{{end}}")
   649  	assert.Nil(app.Views.Initialize())
   650  
   651  	app.GET("/", ok)
   652  	app.GET("/view", viewOK)
   653  	app.Tracer = mockTracer{
   654  		OnStart:  func(_ *Ctx) { wg.Done() },
   655  		OnFinish: func(_ *Ctx, _ error) { wg.Done() },
   656  		OnViewStart: func(ctx *Ctx, vr *ViewResult) {
   657  			defer wg.Done()
   658  			hasValue = vr.ViewName == "ok"
   659  		},
   660  		OnViewFinish: func(ctx *Ctx, vr *ViewResult, err error) {
   661  			defer wg.Done()
   662  		},
   663  	}
   664  
   665  	_, err = MockGet(app, "/view").Discard()
   666  	assert.Nil(err)
   667  	wg.Wait()
   668  
   669  	assert.True(hasValue)
   670  }
   671  
   672  func TestAppViewTracerError(t *testing.T) {
   673  	assert := assert.New(t)
   674  
   675  	wg := sync.WaitGroup{}
   676  	wg.Add(4)
   677  
   678  	var hasValue, hasError, hasViewError bool
   679  
   680  	app, err := New()
   681  	assert.Nil(err)
   682  
   683  	app.Views.AddLiterals("{{ define \"ok\" }}{{template \"fake\"}}ok{{end}}")
   684  	app.GET("/view", viewOK)
   685  	app.Tracer = mockTracer{
   686  		OnStart: func(_ *Ctx) { wg.Done() },
   687  		OnFinish: func(_ *Ctx, err error) {
   688  			defer wg.Done()
   689  			hasError = err != nil
   690  		},
   691  		OnViewStart: func(ctx *Ctx, vr *ViewResult) {
   692  			defer wg.Done()
   693  			hasValue = vr.ViewName == "ok"
   694  		},
   695  		OnViewFinish: func(ctx *Ctx, vr *ViewResult, err error) {
   696  			defer wg.Done()
   697  			hasViewError = err != nil
   698  		},
   699  	}
   700  	_, err = MockGet(app, "/view").Discard()
   701  	assert.Nil(err)
   702  	wg.Wait()
   703  
   704  	assert.True(hasValue)
   705  	assert.True(hasError)
   706  	assert.True(hasViewError)
   707  }
   708  
   709  func TestAppNilLoggerPanic(t *testing.T) {
   710  	assert := assert.New(t)
   711  
   712  	app, err := New(OptLog(nil))
   713  	assert.Nil(err)
   714  	app.PanicAction = func(r *Ctx, err interface{}) Result {
   715  		return r.DefaultProvider.InternalError(ex.New(err))
   716  	}
   717  	assert.Nil(err)
   718  	app.GET("/", doPanic, ViewProviderAsDefault)
   719  
   720  	res, err := MockGet(app, "/").Discard()
   721  	assert.Nil(err)
   722  	assert.Equal(http.StatusInternalServerError, res.StatusCode)
   723  }
   724  
   725  func TestAppContextRequestStarted(t *testing.T) {
   726  	assert := assert.New(t)
   727  
   728  	app, err := New()
   729  	assert.Nil(err)
   730  
   731  	var hadRequestStarted bool
   732  	app.GET("/", func(r *Ctx) Result {
   733  		hadRequestStarted = !GetRequestStarted(r.Context()).IsZero()
   734  		return nil
   735  	})
   736  
   737  	_, err = MockGet(app, "/").Discard()
   738  	assert.Nil(err)
   739  	assert.True(hadRequestStarted)
   740  }
   741  
   742  func TestAppMethodBare(t *testing.T) {
   743  	assert := assert.New(t)
   744  
   745  	buffer := bytes.NewBuffer(nil)
   746  	agent := logger.MustNew(
   747  		logger.OptAll(),
   748  		logger.OptFormatter(
   749  			logger.NewTextOutputFormatter(
   750  				logger.OptTextHideTimestamp(),
   751  				logger.OptTextNoColor(),
   752  			),
   753  		),
   754  		logger.OptOutput(buffer),
   755  	)
   756  
   757  	app, err := New(
   758  		OptLog(agent),
   759  	)
   760  	assert.Nil(err)
   761  
   762  	app.MethodBare(webutil.MethodGet, "/", func(r *Ctx) Result {
   763  		return Raw([]byte("ok!"))
   764  	})
   765  	_, err = MockGet(app, "/").Discard()
   766  	assert.Nil(err)
   767  	agent.Drain()
   768  	assert.Empty(buffer.String())
   769  }