go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/web/mock.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package web
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	"encoding/json"
    14  	"io"
    15  	"net/http"
    16  	"net/http/httptest"
    17  	"net/url"
    18  	"time"
    19  
    20  	"go.charczuk.com/sdk/r2"
    21  )
    22  
    23  // Mock sends a mock request to an app.
    24  //
    25  // It will reset the app Server, Listener, and will set the request host to the listener address
    26  // for a randomized local listener.
    27  func Mock(app *App, req *http.Request, options ...r2.Option) *MockResult {
    28  	r2req := new(r2.Request)
    29  	_ = r2.OptRequest(req)(r2req)
    30  
    31  	var err error
    32  	result := &MockResult{
    33  		App:     app,
    34  		Request: r2req,
    35  	}
    36  	for _, option := range options {
    37  		if err = option(result.Request); err != nil {
    38  			_ = r2.OptErr(err)(result.Request)
    39  			return result
    40  		}
    41  	}
    42  	if err := app.Initialize(); err != nil {
    43  		_ = r2.OptErr(err)(result.Request)
    44  		return result
    45  	}
    46  
    47  	if result.URL() == nil {
    48  		_ = r2.OptURL(new(url.URL))(result.Request)
    49  	}
    50  
    51  	result.Server = httptest.NewUnstartedServer(app)
    52  	// result.Server.Config.BaseContext = app.BaseContext
    53  	result.Server.Start()
    54  	_ = r2.OptCloser(result.Close)(result.Request)
    55  
    56  	parsedServerURL := MustParseURL(result.Server.URL)
    57  	_ = r2.OptScheme(parsedServerURL.Scheme)(result.Request)
    58  	_ = r2.OptHost(parsedServerURL.Host)(result.Request)
    59  	return result
    60  }
    61  
    62  // MockMethod sends a mock request with a given method to an app.
    63  // You should use request options to set the body of the request if it's a post or put etc.
    64  func MockMethod(app *App, method, path string, options ...r2.Option) *MockResult {
    65  	req := &http.Request{
    66  		Method: method,
    67  		URL: &url.URL{
    68  			Path: path,
    69  		},
    70  	}
    71  	return Mock(app, req, options...)
    72  }
    73  
    74  // MockGet sends a mock get request to an app.
    75  func MockGet(app *App, path string, options ...r2.Option) *MockResult {
    76  	req := &http.Request{
    77  		Method: "GET",
    78  		URL: &url.URL{
    79  			Path: path,
    80  		},
    81  	}
    82  	return Mock(app, req, options...)
    83  }
    84  
    85  // MockPost sends a mock post request to an app.
    86  func MockPost(app *App, path string, body io.ReadCloser, options ...r2.Option) *MockResult {
    87  	req := &http.Request{
    88  		Method: "POST",
    89  		Body:   body,
    90  		URL: &url.URL{
    91  			Path: path,
    92  		},
    93  	}
    94  	return Mock(app, req, options...)
    95  }
    96  
    97  // MockPostJSON sends a mock post request with a json body to an app.
    98  func MockPostJSON(app *App, path string, body interface{}, options ...r2.Option) *MockResult {
    99  	contents, _ := json.Marshal(body)
   100  	req := &http.Request{
   101  		Method: "POST",
   102  		Body:   io.NopCloser(bytes.NewReader(contents)),
   103  		URL: &url.URL{
   104  			Path: path,
   105  		},
   106  	}
   107  	return Mock(app, req, options...)
   108  }
   109  
   110  // MockResult is a result of a mocked request.
   111  type MockResult struct {
   112  	*r2.Request
   113  	App    *App
   114  	Server *httptest.Server
   115  }
   116  
   117  // Close stops the app.
   118  func (mr *MockResult) Close() error {
   119  	mr.Server.Close()
   120  	return nil
   121  }
   122  
   123  // MockContext returns a new mock ctx.
   124  // It is intended to be used in testing.
   125  func MockContext(method, path string) Context {
   126  	return MockContextWithBuffer(method, path, new(bytes.Buffer))
   127  }
   128  
   129  // MockContextWithBuffer returns a new mock ctx.
   130  // It is intended to be used in testing.
   131  func MockContextWithBuffer(method, path string, buf io.Writer) Context {
   132  	return &baseContext{
   133  		app: new(App),
   134  		res: NewMockResponse(buf),
   135  		req: NewMockRequest(method, path),
   136  	}
   137  }
   138  
   139  var (
   140  	_ http.ResponseWriter = (*MockResponseWriter)(nil)
   141  	_ http.Flusher        = (*MockResponseWriter)(nil)
   142  )
   143  
   144  // NewMockResponse returns a mocked response writer.
   145  func NewMockResponse(buffer io.Writer) *MockResponseWriter {
   146  	return &MockResponseWriter{
   147  		innerWriter: buffer,
   148  		contents:    new(bytes.Buffer),
   149  		headers:     http.Header{},
   150  	}
   151  }
   152  
   153  // MockResponseWriter is an object that satisfies response writer but uses an internal buffer.
   154  type MockResponseWriter struct {
   155  	innerWriter   io.Writer
   156  	contents      *bytes.Buffer
   157  	statusCode    int
   158  	contentLength int
   159  	headers       http.Header
   160  }
   161  
   162  // Write writes data and adds to ContentLength.
   163  func (res *MockResponseWriter) Write(buffer []byte) (int, error) {
   164  	bytesWritten, err := res.innerWriter.Write(buffer)
   165  	res.contentLength += bytesWritten
   166  	defer func() {
   167  		res.contents.Write(buffer)
   168  	}()
   169  	return bytesWritten, err
   170  }
   171  
   172  // Header returns the response headers.
   173  func (res *MockResponseWriter) Header() http.Header {
   174  	return res.headers
   175  }
   176  
   177  // WriteHeader sets the status code.
   178  func (res *MockResponseWriter) WriteHeader(statusCode int) {
   179  	res.statusCode = statusCode
   180  }
   181  
   182  // InnerResponse returns the backing httpresponse writer.
   183  func (res *MockResponseWriter) InnerResponse() http.ResponseWriter {
   184  	return res
   185  }
   186  
   187  // StatusCode returns the status code.
   188  func (res *MockResponseWriter) StatusCode() int {
   189  	return res.statusCode
   190  }
   191  
   192  // ContentLength returns the content length.
   193  func (res *MockResponseWriter) ContentLength() int {
   194  	return res.contentLength
   195  }
   196  
   197  // Bytes returns the raw response.
   198  func (res *MockResponseWriter) Bytes() []byte {
   199  	return res.contents.Bytes()
   200  }
   201  
   202  // Flush is a no-op.
   203  func (res *MockResponseWriter) Flush() {}
   204  
   205  // Close is a no-op.
   206  func (res *MockResponseWriter) Close() error {
   207  	return nil
   208  }
   209  
   210  // NewMockRequest creates a mock request.
   211  func NewMockRequest(method, path string) *http.Request {
   212  	return &http.Request{
   213  		Method:     method,
   214  		Proto:      "http",
   215  		ProtoMajor: 1,
   216  		ProtoMinor: 1,
   217  		Host:       "localhost:8080",
   218  		RemoteAddr: "127.0.0.1:8080",
   219  		RequestURI: path,
   220  		Header: http.Header{
   221  			HeaderUserAgent: []string{"go-sdk test"},
   222  		},
   223  		URL: &url.URL{
   224  			Scheme:  "http",
   225  			Host:    "localhost",
   226  			Path:    path,
   227  			RawPath: path,
   228  		},
   229  	}
   230  }
   231  
   232  // NewMockRequestWithCookie creates a mock request with a cookie attached to it.
   233  func NewMockRequestWithCookie(method, path, cookieName, cookieValue string) *http.Request {
   234  	req := NewMockRequest(method, path)
   235  	req.AddCookie(&http.Cookie{
   236  		Name:   cookieName,
   237  		Domain: "localhost",
   238  		Path:   "/",
   239  		Value:  cookieValue,
   240  	})
   241  	return req
   242  }
   243  
   244  // MockSimulateLogin simulates a user login for a given app as mocked request params (i.e. r2 options).
   245  //
   246  // This requires an auth manager to be configured to handle and persist sessions.
   247  func MockSimulateLogin(ctx context.Context, app *App, userID string, opts ...r2.Option) []r2.Option {
   248  	sessionID := NewSessionID()
   249  	session := &Session{
   250  		UserID:     userID,
   251  		SessionID:  sessionID,
   252  		CreatedUTC: time.Now().UTC(),
   253  	}
   254  	if persistHandler, ok := app.AuthPersister.(PersistSessionHandler); ok {
   255  		_ = persistHandler.PersistSession(ctx, session)
   256  	}
   257  	return append([]r2.Option{
   258  		r2.OptCookieValue(app.AuthCookieDefaults.Name, sessionID),
   259  	}, opts...)
   260  }