Repository: gobuffalo/buffalo (go)

Issue:
Panic in Simpleworker
Hello,

I'm getting panic in Debug logger when a simple worker completes the job 
```
level=debug time="2022-04-17T22:20:12Z" msg="completed job %!s(PANIC=String method: runtime error: invalid memory address or nil pointer dereference)
```

That's the [culprit](https://github.com/gobuffalo/buffalo/blob/main/worker/simple.go#L103) and this  [code panics too](https://github.com/gobuffalo/buffalo/blob/main/worker/simple.go#L81)



Pull request:
hardening DefaultContext to make it panic-free
Improved DefaultContext since it is exported and has some functions that could make a panic.

* added comment for road-to-v1
* made functions panic-free
  * `DefaultContext.Set()`
  * `DefaultContext.Value()`
  * `DefaultContext.Data()`
  * indirectly `DefaultContext.String()`
  * indirectly `DefaultContext.MarshalJSON()`
* added unit tests for them

fixes #2241


Patch:
diff --git a/default_context.go b/default_context.go
index 3f2eec99..e88acbf4 100644
--- a/default_context.go
+++ b/default_context.go
@@ -23,6 +23,9 @@ import (
 var _ Context = &DefaultContext{}
 var _ context.Context = &DefaultContext{}
 
+// TODO(sio4): #road-to-v1 - make DefaultContext private
+// and only allow it to be generated by App.newContext() or any similar.
+
 // DefaultContext is, as its name implies, a default
 // implementation of the Context interface.
 type DefaultContext struct {
@@ -67,16 +70,22 @@ func (d *DefaultContext) Param(key string) string {
 // Set a value onto the Context. Any value set onto the Context
 // will be automatically available in templates.
 func (d *DefaultContext) Set(key string, value interface{}) {
+	if d.data == nil {
+		d.data = &sync.Map{}
+	}
 	d.data.Store(key, value)
 }
 
 // Value that has previously stored on the context.
 func (d *DefaultContext) Value(key interface{}) interface{} {
-	if k, ok := key.(string); ok {
+	if k, ok := key.(string); ok && d.data != nil {
 		if v, ok := d.data.Load(k); ok {
 			return v
 		}
 	}
+	if d.Context == nil {
+		return nil
+	}
 	return d.Context.Value(key)
 }
 
@@ -235,6 +244,11 @@ func (d *DefaultContext) Redirect(status int, url string, args ...interface{}) e
 // Data contains all the values set through Get/Set.
 func (d *DefaultContext) Data() map[string]interface{} {
 	m := map[string]interface{}{}
+
+	if d.data == nil {
+		return m
+	}
+
 	d.data.Range(func(k, v interface{}) bool {
 		s, ok := k.(string)
 		if !ok {


Test patch:
diff --git a/default_context_test.go b/default_context_test.go
index 2823828c..20378164 100644
--- a/default_context_test.go
+++ b/default_context_test.go
@@ -143,6 +143,15 @@ func Test_DefaultContext_GetSet(t *testing.T) {
 	r.Equal("Mark", c.Value("name").(string))
 }
 
+func Test_DefaultContext_Set_Unconfigured(t *testing.T) {
+	r := require.New(t)
+	c := DefaultContext{}
+
+	c.Set("name", "Yonghwan")
+	r.NotNil(c.Value("name"))
+	r.Equal("Yonghwan", c.Value("name").(string))
+}
+
 func Test_DefaultContext_Value(t *testing.T) {
 	r := require.New(t)
 	c := basicContext()
@@ -151,7 +160,12 @@ func Test_DefaultContext_Value(t *testing.T) {
 	c.Set("name", "Mark")
 	r.NotNil(c.Value("name"))
 	r.Equal("Mark", c.Value("name").(string))
-	r.Equal("Mark", c.Value("name").(string))
+}
+
+func Test_DefaultContext_Value_Unconfigured(t *testing.T) {
+	r := require.New(t)
+	c := DefaultContext{}
+	r.Nil(c.Value("name"))
 }
 
 func Test_DefaultContext_Render(t *testing.T) {
@@ -301,3 +315,68 @@ func Test_DefaultContext_Bind_JSON(t *testing.T) {
 
 	r.Equal("Mark", user.FirstName)
 }
+
+func Test_DefaultContext_Data(t *testing.T) {
+	r := require.New(t)
+	c := basicContext()
+
+	r.EqualValues(map[string]interface{}{}, c.Data())
+}
+
+func Test_DefaultContext_Data_Unconfigured(t *testing.T) {
+	r := require.New(t)
+	c := DefaultContext{}
+
+	r.EqualValues(map[string]interface{}{}, c.Data())
+}
+
+func Test_DefaultContext_String(t *testing.T) {
+	r := require.New(t)
+	c := basicContext()
+	c.Set("name", "Buffalo")
+	c.Set("language", "go")
+
+	r.EqualValues("language: go\n\nname: Buffalo", c.String())
+}
+
+func Test_DefaultContext_String_EmptyData(t *testing.T) {
+	r := require.New(t)
+	c := basicContext()
+	r.EqualValues("", c.String())
+}
+
+func Test_DefaultContext_String_EmptyData_Unconfigured(t *testing.T) {
+	r := require.New(t)
+	c := DefaultContext{}
+
+	r.EqualValues("", c.String())
+}
+
+func Test_DefaultContext_MarshalJSON(t *testing.T) {
+	r := require.New(t)
+	c := basicContext()
+	c.Set("name", "Buffalo")
+	c.Set("language", "go")
+
+	jb, err := c.MarshalJSON()
+	r.NoError(err)
+	r.EqualValues(`{"language":"go","name":"Buffalo"}`, string(jb))
+}
+
+func Test_DefaultContext_MarshalJSON_EmptyData(t *testing.T) {
+	r := require.New(t)
+	c := basicContext()
+
+	jb, err := c.MarshalJSON()
+	r.NoError(err)
+	r.EqualValues(`{}`, string(jb))
+}
+
+func Test_DefaultContext_MarshalJSON_EmptyData_Unconfigured(t *testing.T) {
+	r := require.New(t)
+	c := DefaultContext{}
+
+	jb, err := c.MarshalJSON()
+	r.NoError(err)
+	r.EqualValues(`{}`, string(jb))
+}