Check out my fine AI art
4 |
├── .github
├── CONTRIBUTING.md
├── README.md
└── workflows
│ └── go.yaml
├── .gitignore
├── LICENSE
├── coco.go
├── coco_test.go
├── context.go
├── context_test.go
├── examples
├── .DS_Store
├── basic
│ ├── grape.json
│ └── main.go
├── with-static-assets
│ ├── grape.json
│ ├── main.go
│ └── web
│ │ ├── assets
│ │ ├── css
│ │ │ └── main.css
│ │ ├── img
│ │ │ └── dimkpa.jpg
│ │ └── js
│ │ │ └── main.js
│ │ └── views
│ │ ├── home.html
│ │ ├── includes
│ │ ├── footer.html
│ │ └── head.html
│ │ └── layout.html
└── with-view
│ ├── main.go
│ └── views
│ ├── about.html
│ ├── dash
│ ├── index.html
│ └── layout.html
│ ├── includes
│ └── header.html
│ ├── index.html
│ └── layout.html
├── go.mod
├── go.sum
├── render.go
├── render_test.go
├── request.go
├── request_test.go
├── response.go
├── response_test.go
├── route.go
└── route_test.go
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Welcome to the coco community! Here's how you can contribute:
4 |
5 | ## Initiating Contributions
6 |
7 | ### New Proposals
8 |
9 | - **Create an Issue**: For new proposals, please create an [issue](github.com/tobolabs/coco/issues) to discuss your ideas with the community.
10 |
11 | ### Existing Issues
12 |
13 | - **Join the Conversation**: For contributions related to existing [issues](https://github.com/tobolabs/coco/issues), feel free to engage directly in the issue tracker.
14 |
15 | ## Pull Requests and Commit Guidelines
16 |
17 | ### Commit Title Conventions
18 |
19 | Use specific prefixes for your commit titles:
20 |
21 | - **feature**: Introducing new functionalities.
22 | - **refactor**: Improving existing code.
23 | - **fix**: Addressing bugs or issues.
24 | - **test**: Adding or updating tests.
25 | - **docs**: Making changes to documentation.
26 | - **fmt**: Enhancing code format and style.
27 |
28 | ### Examples
29 |
30 | - `feature: add new feature`
31 | - `refactor: improve existing code`
32 | - `fix: address bug or issue`
33 | - `test: add or update tests`
34 | - `docs: make changes to documentation`
35 | - `fmt: enhance code format and style`
36 |
37 | ## Supporting coco
38 |
39 | ### How You Can Help
40 |
41 | Thank you for your interest in contributing to coco. Your efforts help us build and maintain a vibrant, thriving community!
42 |
43 | 1. **Star on GitHub**: Show your support by starring [coco](https://github.com/tobolabs/coco/stargazers).
44 | 2. **Spread the Word**: Share your experiences with coco [on Twitter](https://x.com/intent/tweet?text=Checkout%20coco%20%28https%3A%2F%2Fgithub.com%2Ftobolabs%2Fcoco%29%20-%20A%20lightweight%20go%20web%20framework%20just%20like%20express%20).
45 |
46 | Thank you for your interest in contributing to coco. Your efforts help us build and maintain a vibrant, thriving community!
47 |
--------------------------------------------------------------------------------
/.github/README.md:
--------------------------------------------------------------------------------
1 |
2 | # coco
3 |
4 | coco is a lightweight, flexible web framework for Go that provides a simple yet powerful API for building web applications.
5 |
6 | [](https://goreportcard.com/report/github.com/tobolabs/coco)
7 | [](http://godoc.org/github.com/tobolabs/coco)
8 | [](https://github.com/tobolabs/coco/releases)
9 |
10 | ## Getting Started
11 |
12 | To get started with coco, you need to have Go installed on your machine. In your go module run:
13 |
14 | ```bash
15 | go get github.com/tobolabs/coco
16 | ```
17 |
18 | ## Features 🚀
19 |
20 | - 🛣️ Dynamic and static routing with `httprouter`.
21 | - 📦 Middleware support for flexible request handling.
22 | - 📑 Template rendering with custom layout configurations.
23 | - 🛠️ Simple API for managing HTTP headers, cookies, and responses.
24 | - 🔄 Graceful shutdown for maintaining service integrity.
25 | - 🔄 JSON and form data handling.
26 |
27 | ## Basic Example
28 |
29 | ```go
30 | package main
31 |
32 | import (
33 | "github.com/tobolabs/coco"
34 | "net/http"
35 | )
36 |
37 | func main() {
38 | app := coco.NewApp()
39 | app.Get("/", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
40 | res.Send("Welcome to coco!")
41 | })
42 | app.Listen(":8080")
43 | }
44 |
45 | ```
46 |
47 | ## API Overview
48 |
49 | ### Routing
50 |
51 | Define routes easily with methods corresponding to HTTP verbs:
52 |
53 | ```go
54 | app.Get("/users", getUsers)
55 | app.Post("/users", createUser)
56 | app.Put("/users/:id", updateUser)
57 | app.Delete("/users/:id", deleteUser)
58 |
59 | ```
60 |
61 | ### Parameter Routing
62 |
63 | ```go
64 | app.Param("id", func(res coco.Response, req *coco.Request, next coco.NextFunc, param string) {
65 | // runs for any route with a param named :id
66 | next(res, req)
67 | })
68 | ```
69 |
70 | ### Middleware
71 |
72 | ```go
73 | app.Use(func(res coco.Response, req *coco.Request, next coco.NextFunc) {
74 | // Log, authenticate, etc.
75 | next(res, req)
76 | })
77 |
78 | ```
79 |
80 | ### Dynamic URL Parameters
81 |
82 | ```go
83 | app.Get("/greet/:name", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
84 | name := req.GetParam("name")
85 | res.Send("Hello, " + name + "!")
86 | })
87 | ```
88 |
89 | ### Settings and Custom Configuration
90 |
91 | ```go
92 | app.SetSetting("x-powered-by", false)
93 | isEnabled := app.IsSettingEnabled("x-powered-by")
94 | ```
95 |
96 | ### Responses
97 |
98 | ```go
99 | app.Get("/data", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
100 | data := map[string]interface{}{"key": "value"}
101 | res.JSON(data)
102 | })
103 | ```
104 |
105 | ### Templates
106 |
107 | ```go
108 | var (
109 | //go:embed views
110 | views embed.FS
111 | )
112 |
113 | app.LoadTemplates(views, nil)
114 | app.Get("/", func(rw coco.Response, r *coco.Request, next coco.NextFunc) {
115 | rw.Render("index", map[string]interface{}{"title": "Home"})
116 | })
117 | ```
118 |
119 | ### Static Files
120 |
121 | ```go
122 | app.Static(http.Dir("public"), "/static")
123 | ```
124 |
125 | ## Acknowledgments
126 |
127 | coco is inspired by [Express](https://expressjs.com/), a popular web framework for Node.js.
128 | At the core of coco is httprouter, a fast HTTP router by [Julien Schmidt](https://github.com/julienschmidt).
129 |
130 | ## Author
131 |
132 | - [Uche Ukwa](https://github.com/noelukwa)
133 |
--------------------------------------------------------------------------------
/.github/workflows/go.yaml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | pull_request:
5 | branches: [ main ]
6 | paths-ignore:
7 | - '**.md'
8 |
9 | jobs:
10 |
11 | test:
12 | name: Test
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Set up Go 1.18
16 | uses: actions/setup-go@v2
17 | with:
18 | go-version: '1.18'
19 |
20 | - name: Check out code into the Go module directory
21 | uses: actions/checkout@v2
22 | - name: Get dependencies
23 | run: go mod download
24 | - name: Check module is ready
25 | run: go mod tidy
26 | - name: Verify module
27 | run: go mod verify
28 | - name: Test
29 | run: go test -v ./...
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 |
20 | # Go workspace file
21 | go.work
22 |
23 | examples/with-template
24 | examples/file-upload
25 | examples/util
26 | .idea
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License Copyright (c) 2023 noelukwa
2 |
3 | Permission is hereby granted, free of
4 | charge, to any person obtaining a copy of this software and associated
5 | documentation files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use, copy, modify, merge,
7 | publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to the
9 | following conditions:
10 |
11 | The above copyright notice and this permission notice
12 | (including the next paragraph) shall be included in all copies or substantial
13 | portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/coco.go:
--------------------------------------------------------------------------------
1 | package coco
2 |
3 | import (
4 | coreCtx "context"
5 | "fmt"
6 | "html/template"
7 | "net/http"
8 | "sync"
9 | "time"
10 |
11 | "github.com/julienschmidt/httprouter"
12 | )
13 |
14 | // NextFunc is a function that is called to pass execution to the next handler
15 | // in the chain.
16 | type NextFunc func(res Response, r *Request)
17 |
18 | // Handler Handle is a function that is called when a request is made to the route.
19 | type Handler func(res Response, req *Request, next NextFunc)
20 |
21 | // ParamHandler is a function that is called when a parameter is found in the route path
22 | type ParamHandler func(res Response, req *Request, next NextFunc, param string)
23 |
24 | // App is the main type for the coco framework.
25 | type App struct {
26 | router *httprouter.Router
27 | httpHandler http.Handler
28 | basePath string
29 | *route
30 | httpServer *http.Server
31 | templates map[string]*template.Template
32 | settings map[string]interface{}
33 | once sync.Once
34 | settingsMutex sync.RWMutex
35 | }
36 |
37 | // Settings returns the settings instance for the App.
38 | func (a *App) Settings() map[string]interface{} {
39 | a.settingsMutex.RLock()
40 | defer a.settingsMutex.RUnlock()
41 | return a.settings
42 | }
43 |
44 | func defaultSettings() map[string]interface{} {
45 | return map[string]interface{}{
46 | "x-powered-by": true,
47 | "env": "development",
48 | "etag": "weak",
49 | "trust proxy": false,
50 | "subdomain offset": 2,
51 | }
52 | }
53 |
54 | // NewApp creates a new App instance with a default Route at the root path "/"
55 | // and a default settings instance with default values.
56 | func NewApp() (app *App) {
57 |
58 | app = &App{
59 | basePath: "",
60 | router: httprouter.New(),
61 | httpHandler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
62 | http.Error(w, "Handler not configured", http.StatusInternalServerError)
63 | }),
64 | settings: defaultSettings(),
65 | }
66 |
67 | app.route = app.newRoute(app.basePath, true, nil)
68 | return
69 | }
70 |
71 | // Listen starts an HTTP server and listens on the given address.
72 | // addr should be in format :PORT ie :8000
73 | func (a *App) Listen(addr string) error {
74 | a.httpServer = &http.Server{
75 | Addr: addr,
76 | Handler: a,
77 | }
78 | return a.httpServer.ListenAndServe()
79 | }
80 |
81 | func (a *App) ServeHTTP(w http.ResponseWriter, req *http.Request) {
82 | a.once.Do(func() {
83 | a.configureRoutes()
84 | a.httpHandler = a.router
85 | })
86 | a.httpHandler.ServeHTTP(w, req)
87 | }
88 |
89 | // Close stops the server gracefully and returns any encountered error.
90 | func (a *App) Close() error {
91 | if a.httpServer == nil {
92 | return nil
93 | }
94 | shutdownTimeout := 5 * time.Second
95 | shutdownCtx, cancel := coreCtx.WithTimeout(coreCtx.Background(), shutdownTimeout)
96 | defer cancel()
97 | err := a.httpServer.Shutdown(shutdownCtx)
98 | if err != nil {
99 | if err == http.ErrServerClosed {
100 | return nil
101 | }
102 | return fmt.Errorf("error during server shutdown: %w", err)
103 | }
104 | return nil
105 | }
106 |
107 | // configureRoutes method attaches the routes to their relevant handlers and middleware
108 | func (a *App) configureRoutes() {
109 | a.traverseAndConfigure(a.route)
110 | }
111 |
112 | func (a *App) traverseAndConfigure(r *route) {
113 | for _, path := range r.paths {
114 | handlers := r.combineHandlers(path.handlers...)
115 | r.hr.Handle(path.method, path.name, func(w http.ResponseWriter, req *http.Request, p httprouter.Params) {
116 | request, err := newRequest(req, w, p, a)
117 | if err != nil {
118 | fmt.Printf("DEBUG: %v\n", err)
119 | }
120 | ctx := &context{
121 | handlers: handlers,
122 | templates: r.app.templates,
123 | req: request,
124 | }
125 | response := Response{ww: wrapWriter(w), ctx: ctx}
126 | execParamChain(ctx, p, r.paramHandlers)
127 | ctx.next(response, request)
128 | })
129 | }
130 | for _, child := range r.children {
131 | a.traverseAndConfigure(child)
132 | }
133 | }
134 |
135 | // SetSetting sets a custom setting with a key and value.
136 | func (a *App) SetSetting(key string, value interface{}) {
137 | a.settingsMutex.Lock()
138 | defer a.settingsMutex.Unlock()
139 | a.settings[key] = value
140 | }
141 |
142 | // GetSetting retrieves a custom setting by its key.
143 | func (a *App) GetSetting(key string) interface{} {
144 | a.settingsMutex.RLock()
145 | defer a.settingsMutex.RUnlock()
146 | return a.settings[key]
147 | }
148 |
149 | // IsSettingEnabled checks if a setting is true.
150 | func (a *App) IsSettingEnabled(key string) bool {
151 | a.settingsMutex.RLock()
152 | defer a.settingsMutex.RUnlock()
153 | value, ok := a.settings[key].(bool)
154 | return ok && value
155 | }
156 |
157 | // IsSettingDisabled checks if a setting is false.
158 | func (a *App) IsSettingDisabled(key string) bool {
159 | a.settingsMutex.RLock()
160 | defer a.settingsMutex.RUnlock()
161 | value, ok := a.settings[key].(bool)
162 | return ok && !value
163 | }
164 |
--------------------------------------------------------------------------------
/coco_test.go:
--------------------------------------------------------------------------------
1 | package coco_test
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | "net/http/httptest"
7 | "reflect"
8 | "testing"
9 |
10 | "github.com/tobolabs/coco/v2"
11 | )
12 |
13 | func TestNewApp(t *testing.T) {
14 | app := coco.NewApp()
15 | if app == nil {
16 | t.Fatal("NewApp() should create a new app instance, got nil")
17 | }
18 |
19 | expectedSettings := map[string]interface{}{
20 | "x-powered-by": true,
21 | "env": "development",
22 | "etag": "weak",
23 | "trust proxy": false,
24 | "subdomain offset": 2,
25 | }
26 |
27 | if !reflect.DeepEqual(app.Settings(), expectedSettings) {
28 | t.Errorf("Expected default settings %v, got %v", expectedSettings, app.Settings())
29 | }
30 | }
31 |
32 | func TestCocoApp_RoutingAndSettings(t *testing.T) {
33 | app := coco.NewApp()
34 | setupAppRoutes(app)
35 | server := httptest.NewServer(app)
36 | defer server.Close()
37 |
38 | testRoutes := []struct {
39 | method string
40 | path string
41 | expectedBody string
42 | expectedStatus int
43 | }{
44 | {"GET", "/one", "1", 200},
45 | {"POST", "/two", "2", 200},
46 | {"DELETE", "/three", "3", 200},
47 | {"PATCH", "/four", "4", 200},
48 | {"PUT", "/five", "5", 200},
49 | {"ALL", "/chow", "choww", 200},
50 | {"OPTIONS", "/six", "6", 200},
51 | {"GET", "/test/123", "123", 200},
52 | }
53 |
54 | for _, test := range testRoutes {
55 | testRouteMethod(t, server.URL, test.method, test.path, test.expectedBody, test.expectedStatus)
56 | }
57 |
58 | // Test settings modifications
59 | app.SetSetting("test", "test")
60 | if app.GetSetting("test") != "test" {
61 | t.Errorf("Expected setting 'test' to be 'test', but got %s", app.GetSetting("test"))
62 | }
63 |
64 | app.SetSetting("someBool", true)
65 | if !app.IsSettingEnabled("someBool") {
66 | t.Errorf("Expected 'someBool' setting to be enabled, but it is not")
67 | }
68 |
69 | app.SetSetting("someBool", false)
70 | if !app.IsSettingDisabled("someBool") {
71 | t.Errorf("Expected 'someBool' setting to be disabled, but it is not")
72 | }
73 | }
74 |
75 | func testRouteMethod(t *testing.T, host, method, path, expectedBody string, expectedStatus int) {
76 | t.Helper()
77 | t.Run(method+" "+path, func(t *testing.T) {
78 | url := host + path
79 | var resp *http.Response
80 | var err error
81 |
82 | switch method {
83 | case "ALL":
84 | methods := []string{"GET", "POST", "DELETE", "PATCH", "PUT"}
85 | for _, m := range methods {
86 | req, _ := http.NewRequest(m, url, nil)
87 | resp, err = http.DefaultClient.Do(req)
88 | checkResponse(t, m, resp, err, expectedBody, expectedStatus)
89 | }
90 | default:
91 | req, _ := http.NewRequest(method, url, nil)
92 | resp, err = http.DefaultClient.Do(req)
93 | checkResponse(t, method, resp, err, expectedBody, expectedStatus)
94 | }
95 | })
96 | }
97 |
98 | func checkResponse(t *testing.T, method string, resp *http.Response, err error, expectedBody string, expectedStatus int) {
99 | if err != nil {
100 | t.Fatalf("Error making %s request: %v", method, err)
101 | }
102 | defer resp.Body.Close()
103 |
104 | if resp.StatusCode != expectedStatus {
105 | t.Errorf("Expected status code %d, but got %d", expectedStatus, resp.StatusCode)
106 | }
107 |
108 | body, err := io.ReadAll(resp.Body)
109 | if err != nil {
110 | t.Fatalf("Error reading response body: %v", err)
111 | }
112 |
113 | actualBody := string(body)
114 | if actualBody != expectedBody {
115 | t.Errorf("Expected response body '%s', but got '%s'", expectedBody, actualBody)
116 | }
117 | }
118 |
119 | func setupAppRoutes(app *coco.App) {
120 | app.All("/chow", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
121 | res.Send("choww")
122 | }).Get("/zero", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
123 | res.Send("0")
124 | }).Get("/one", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
125 | res.Send("1")
126 | }).Post("/two", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
127 | res.Send("2")
128 | }).Delete("/three", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
129 | res.Send("3")
130 | }).Patch("/four", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
131 | res.Send("4")
132 | }).Put("/five", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
133 | res.Send("5")
134 | }).Options("/six", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
135 | res.Send("6")
136 | }).Param("id", func(res coco.Response, req *coco.Request, next coco.NextFunc, param string) {
137 | res.Send(param)
138 | }).Get("/test/:id", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
139 | res.Send("!!")
140 | }).Get("/echo", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
141 | res.Send("Echo test")
142 | })
143 | }
144 |
145 | func TestApp_Settings(t *testing.T) {
146 | app := coco.NewApp()
147 | app.SetSetting("x-powered-by", false)
148 | if !app.IsSettingDisabled("x-powered-by") {
149 | t.Errorf("Expected 'x-powered-by' to be disabled")
150 | }
151 |
152 | app.SetSetting("x-powered-by", true)
153 | if !app.IsSettingEnabled("x-powered-by") {
154 | t.Errorf("Expected 'x-powered-by' to be enabled")
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/context.go:
--------------------------------------------------------------------------------
1 | package coco
2 |
3 | import (
4 | "html/template"
5 | "net/http"
6 | )
7 |
8 | type context struct {
9 | handlers []Handler
10 | templates map[string]*template.Template
11 | req *Request
12 | app *App
13 | }
14 |
15 | func (c *context) coco() *App {
16 | return c.app
17 | }
18 |
19 | func (c *context) request() *http.Request {
20 | return c.req.r
21 | }
22 |
23 | // next calls the next handler in the chain if there is one.
24 | // If there is no next handler, the request is terminated.
25 | func (c *context) next(rw Response, req *Request) {
26 | if len(c.handlers) == 0 {
27 | http.NotFound(rw.ww, req.r)
28 | return
29 | }
30 | h := c.handlers[0]
31 | c.handlers = c.handlers[1:]
32 | h(rw, req, c.next)
33 | }
34 |
--------------------------------------------------------------------------------
/context_test.go:
--------------------------------------------------------------------------------
1 | package coco
2 |
3 | import (
4 | "net/http/httptest"
5 | "reflect"
6 | "testing"
7 | )
8 |
9 | func Test_context_coco(t *testing.T) {
10 | app := &App{}
11 | rc := &context{app: app}
12 |
13 | if got := rc.coco(); got != app {
14 | t.Errorf("rcontext.coco() = %v, want %v", got, app)
15 | }
16 | }
17 |
18 | func Test_context_next_noHandlers(t *testing.T) {
19 | rc := &context{handlers: []Handler{}}
20 | rw := Response{ww: wrapWriter(httptest.NewRecorder())}
21 | req := &Request{r: httptest.NewRequest("GET", "/", nil)}
22 |
23 | rc.next(rw, req)
24 |
25 | if len(rc.handlers) != 0 {
26 | t.Error("Expected no handlers to be removed from the slice")
27 | }
28 | }
29 |
30 | func Test_context_next_oneHandler(t *testing.T) {
31 | called := false
32 | handler := func(rw Response, req *Request, next NextFunc) {
33 | called = true
34 | }
35 |
36 | rc := &context{handlers: []Handler{handler}}
37 | rw := Response{ww: wrapWriter(httptest.NewRecorder())}
38 | req := &Request{r: httptest.NewRequest("GET", "/", nil)}
39 |
40 | rc.next(rw, req)
41 |
42 | if !called {
43 | t.Error("Expected the handler to be called")
44 | }
45 |
46 | if len(rc.handlers) != 0 {
47 | t.Error("Expected the handler to be removed from the slice")
48 | }
49 | }
50 |
51 | func Test_context_next_multipleHandlers(t *testing.T) {
52 | var callOrder []int
53 | handler1 := func(rw Response, req *Request, next NextFunc) {
54 | callOrder = append(callOrder, 1)
55 | next(rw, req)
56 | }
57 | handler2 := func(rw Response, req *Request, next NextFunc) {
58 | callOrder = append(callOrder, 2)
59 | }
60 |
61 | rc := &context{handlers: []Handler{handler1, handler2}}
62 | rw := Response{ww: wrapWriter(httptest.NewRecorder())}
63 | req := &Request{r: httptest.NewRequest("GET", "/", nil)}
64 |
65 | rc.next(rw, req)
66 |
67 | if !reflect.DeepEqual(callOrder, []int{1, 2}) {
68 | t.Errorf("Handlers were called in the wrong order: got %v, want %v", callOrder, []int{1, 2})
69 | }
70 |
71 | if len(rc.handlers) != 0 {
72 | t.Error("Expected all handlers to be removed from the slice")
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/examples/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tobolabs/coco/e68bc93f39f4e2ef7a9125d6b52c030204d811ae/examples/.DS_Store
--------------------------------------------------------------------------------
/examples/basic/grape.json:
--------------------------------------------------------------------------------
1 | {
2 | "dev": {
3 | "watch": {
4 | "exclude": [
5 | "vendor"
6 | ],
7 | "include": [
8 | "*.go"
9 | ]
10 | },
11 | "run": "go run ."
12 | }
13 | }
--------------------------------------------------------------------------------
/examples/basic/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 |
8 | "github.com/tobolabs/coco/v2"
9 | )
10 |
11 | func main() {
12 | app := coco.NewApp()
13 |
14 | app.Param("id", func(res coco.Response, req *coco.Request, next coco.NextFunc, param string) {
15 | fmt.Printf("Param Middleware: %s\n", param)
16 | next(res, req)
17 | })
18 |
19 | app.Post("/post", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
20 |
21 | data, err := req.Body.FormData()
22 | if err != nil {
23 | res.Send(err.Error())
24 | return
25 | }
26 | res.JSON(data)
27 | })
28 |
29 | // Application.All ✅
30 | app.All("/generic", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
31 | res.Send("Generic")
32 | })
33 |
34 | // Application.Delete ✅
35 | app.Delete("/delete", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
36 | res.Send("Delete")
37 | })
38 |
39 | // Application.Disable ✅
40 | app.SetSetting("x-powered-by", false)
41 |
42 | // Application.Disabled ✅
43 | fmt.Printf("Disabled: %v\n", app.IsSettingDisabled("x-powered-by"))
44 |
45 | app.Get("/chekme", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
46 |
47 | res.Cookie(&http.Cookie{
48 | Name: "greeting",
49 | Value: "Ser",
50 | })
51 | res.Send("Checkme")
52 | })
53 |
54 | app.Use(func(res coco.Response, req *coco.Request, next coco.NextFunc) {
55 |
56 | isJson := req.Is("json/*")
57 | fmt.Printf("isJson: %v\n", isJson)
58 |
59 | isText := req.Is("text/*")
60 | fmt.Printf("isText: %v\n", isText)
61 |
62 | isHtml := req.Is("html")
63 | fmt.Printf("isHtml: %v\n", isHtml)
64 |
65 | next(res, req)
66 | })
67 |
68 | app.Get("hello/:id", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
69 | res.Send(fmt.Sprintf("Hello %s 👋", req.Params["id"]))
70 | })
71 |
72 | app.Post("hello/:id", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
73 | res.Send(fmt.Sprintf("Hello %s 👋", req.Params["id"]))
74 | })
75 |
76 | app.Get("hello", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
77 | res.Send("Hello World")
78 | })
79 |
80 | app.Post("hello", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
81 | res.Send("Hello World Post")
82 | })
83 |
84 | userRouter := app.NewRouter("users")
85 |
86 | userRouter.Param("id", func(res coco.Response, req *coco.Request, next coco.NextFunc, param string) {
87 | log.Println("User Param Middleware")
88 | next(res, req)
89 | })
90 |
91 | userRouter.Get("/", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
92 | fmt.Printf("req.BaseUrl : %s\n", req.BaseURL)
93 | res.Send("Hello User")
94 | })
95 |
96 | userRouter.Use(func(res coco.Response, req *coco.Request, next coco.NextFunc) {
97 | log.Println("User Middleware 1")
98 | next(res, req)
99 | })
100 |
101 | userRouter.Get("hello/:id", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
102 | res.Send(fmt.Sprintf("Hello %s 👋", req.Params["id"]))
103 | })
104 |
105 | profileRouter := userRouter.NewRouter("profile")
106 |
107 | profileRouter.Use(func(res coco.Response, req *coco.Request, next coco.NextFunc) {
108 | log.Println("Profile Middleware 1")
109 | next(res, req)
110 | })
111 |
112 | profileRouter.Get("/", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
113 | fmt.Printf("req.BaseUrl : %s\n", req.BaseURL)
114 | res.Send("Hello User Profile")
115 | })
116 |
117 | userRouter.Get("/profile/settings", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
118 | fmt.Println(req.Path)
119 | fmt.Printf("req.BaseUrl : %s\n", req.BaseURL)
120 | res.Send("Hello User")
121 | })
122 |
123 | socialRouter := userRouter.NewRouter("social")
124 |
125 | socialRouter.Get("/", func(res coco.Response, req *coco.Request, next coco.NextFunc) {
126 | fmt.Printf("req.BaseUrl : %s\n", req.BaseURL)
127 | res.Send("Hello User Social")
128 |
129 | })
130 |
131 | srv := &http.Server{
132 | Addr: ":3003",
133 | Handler: app,
134 | }
135 |
136 | if err := srv.ListenAndServe(); err != nil {
137 | log.Fatal(err.Error())
138 | }
139 | //
140 | //if err := app.Listen(":3003"); err != nil {
141 | // log.Fatal(err.Error())
142 | //}
143 | }
144 |
--------------------------------------------------------------------------------
/examples/with-static-assets/grape.json:
--------------------------------------------------------------------------------
1 | {
2 | "dev": {
3 | "watch": {
4 | "exclude": [
5 | "vendor"
6 | ],
7 | "include": [
8 | "*.go"
9 | ]
10 | },
11 | "run": "go run ."
12 | }
13 | }
--------------------------------------------------------------------------------
/examples/with-static-assets/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "embed"
5 | "io/fs"
6 | "log"
7 |
8 | "github.com/tobolabs/coco/v2"
9 | )
10 |
11 | var (
12 | //go:embed web/views
13 | views embed.FS
14 |
15 | //go:embed web/assets
16 | assets embed.FS
17 | )
18 |
19 | func main() {
20 |
21 | app := coco.NewApp()
22 |
23 | if err := app.LoadTemplates(views, nil); err != nil {
24 | log.Fatal(err)
25 | }
26 |
27 | staticFiles, err := fs.Sub(assets, "web/assets")
28 | if err != nil {
29 | log.Fatalf("Failed to create sub FS: %v", err)
30 | }
31 |
32 | // Serve static assets on /static; e.g. /static/css/main.css maps to web/assets/css/main.css
33 | app.Static(staticFiles, "/static")
34 |
35 | app.Get("/", func(rw coco.Response, req *coco.Request, next coco.NextFunc) {
36 | rw.Render("views/home", nil)
37 | })
38 |
39 | if err := app.Listen(":8980"); err != nil {
40 | log.Fatal(err)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/examples/with-static-assets/web/assets/css/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #0ebcb6;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
4 | }
5 |
6 | p {
7 | color: #333;
8 | }
--------------------------------------------------------------------------------
/examples/with-static-assets/web/assets/img/dimkpa.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tobolabs/coco/e68bc93f39f4e2ef7a9125d6b52c030204d811ae/examples/with-static-assets/web/assets/img/dimkpa.jpg
--------------------------------------------------------------------------------
/examples/with-static-assets/web/assets/js/main.js:
--------------------------------------------------------------------------------
1 |
2 | const button = document.getElementById('ctx-button')
3 |
4 | button.addEventListener('click', function () {
5 | alert('Hello from the client!')
6 | })
7 |
--------------------------------------------------------------------------------
/examples/with-static-assets/web/views/home.html:
--------------------------------------------------------------------------------
1 | {{define "body"}}
2 | Check out my fine AI art
6 |
Admin Page Home
4 |Home Page
4 |Layout fallback!
{{end}} 12 | 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tobolabs/coco/v2 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 7 | github.com/julienschmidt/httprouter v1.3.0 8 | github.com/noelukwa/tempest v1.2.0 9 | github.com/spf13/afero v1.10.0 10 | github.com/stretchr/testify v1.7.0 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | golang.org/x/text v0.3.7 // indirect 18 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 41 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 42 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 43 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 44 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 45 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 46 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 47 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 48 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 49 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 50 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 51 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 52 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 53 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 54 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 55 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 56 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 57 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 58 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 59 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 60 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 61 | github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 h1:O6yi4xa9b2DMosGsXzlMe2E9qXgXCVkRLCoRX+5amxI= 62 | github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27/go.mod h1:AYvN8omj7nKLmbcXS2dyABYU6JB1Lz1bHmkkq1kf4I4= 63 | github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno= 64 | github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= 65 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 66 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 67 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 68 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 69 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 70 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 71 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 72 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 73 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 74 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 75 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 76 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 77 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 78 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 79 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 80 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 81 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 82 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 83 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 84 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 85 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 86 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 87 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 88 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 89 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 90 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 91 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 92 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 93 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 94 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 95 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 96 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 97 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 98 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 99 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 100 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 101 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 102 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 103 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 104 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 105 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 106 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 107 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 108 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 109 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 110 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 111 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 112 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 113 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 114 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 115 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 116 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 117 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 118 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 119 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 120 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 121 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 122 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 123 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 124 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 125 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 126 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 127 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 128 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 129 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 130 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 131 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 132 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 133 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 134 | github.com/noelukwa/tempest v1.2.0 h1:cYetz8eBDddXEyY1BtrIhjIfdJdA0wOizubpnftY0Sw= 135 | github.com/noelukwa/tempest v1.2.0/go.mod h1:xKE4PXHimHsiohHlGcoumLMIF+UGG/0DHY69iG1VYdw= 136 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 137 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 138 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 139 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 140 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 141 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 142 | github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= 143 | github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= 144 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 145 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 146 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 147 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 148 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 149 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 150 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 151 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 152 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 153 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 154 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 155 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 156 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 157 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 158 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 159 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 160 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 161 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 162 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 163 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 164 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 165 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 166 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 167 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 168 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 169 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 170 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 171 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 172 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 173 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 174 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 175 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 176 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 177 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 178 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 179 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 180 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 181 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 182 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 183 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 184 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 185 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 186 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 187 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 188 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 189 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 190 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 191 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 192 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 193 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 194 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 195 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 196 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 197 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 198 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 199 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 200 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 201 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 202 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 203 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 204 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 205 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 206 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 207 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 208 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 209 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 210 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 211 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 212 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 213 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 214 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 215 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 216 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 217 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 218 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 219 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 220 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 221 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 222 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 223 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 224 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 225 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 226 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 227 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 228 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 229 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 230 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 231 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 232 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 233 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 234 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 235 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 236 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 237 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 238 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 239 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 240 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 241 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 242 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 243 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 244 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 245 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 246 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 247 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 248 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 249 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 250 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 251 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 252 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 253 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 254 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 255 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 256 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 257 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 258 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 259 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 260 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 261 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 262 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 263 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 264 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 265 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 266 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 267 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 268 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 269 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 270 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 271 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 272 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 273 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 274 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 275 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 276 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 277 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 278 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 279 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 280 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 281 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 282 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 283 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 284 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 285 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 286 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 287 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 288 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 289 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 290 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 291 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 292 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 293 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 294 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 295 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 296 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 297 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 298 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 299 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 300 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 301 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 302 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 303 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 304 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 305 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 306 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 307 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 308 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 309 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 310 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 311 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 312 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 313 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 314 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 315 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 316 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 317 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 318 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 319 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 320 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 321 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 322 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 323 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 324 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 325 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 326 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 327 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 328 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 329 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 330 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 331 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 332 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 333 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 334 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 335 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 336 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 337 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 338 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 339 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 340 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 341 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 342 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 343 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 344 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 345 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 346 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 347 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 348 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 349 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 350 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 351 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 352 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 353 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 354 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 355 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 356 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 357 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 358 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 359 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 360 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 361 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 362 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 363 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 364 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 365 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 366 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 367 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 368 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 369 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 370 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 371 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 372 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 373 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 374 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 375 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 376 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 377 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 378 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 379 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 380 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 381 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 382 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 383 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 384 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 385 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 386 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 387 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 388 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 389 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 390 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 391 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 392 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 393 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 394 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 395 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 396 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 397 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 398 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 399 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 400 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 401 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 402 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 403 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 404 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 405 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 406 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 407 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 408 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 409 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 410 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 411 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 412 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 413 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 414 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 415 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 416 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 417 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 418 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 419 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 420 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 421 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 422 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 423 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 424 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 425 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 426 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 427 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 428 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 429 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 430 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 431 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 432 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 433 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 434 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 435 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 436 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 437 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 438 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 439 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 440 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 441 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 442 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 443 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 444 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 445 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 446 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 447 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 448 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 449 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 450 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 451 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 452 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 453 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 454 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package coco 2 | 3 | import ( 4 | "html/template" 5 | "io/fs" 6 | 7 | "github.com/noelukwa/tempest" 8 | ) 9 | 10 | // TemplateConfig is a configuration for loading templates from an fs.FS 11 | type TemplateConfig struct { 12 | // The file extension of the templates. 13 | // Defaults to ".html". 14 | Ext string 15 | 16 | // The directory where the includes are stored. 17 | // Defaults to "includes". 18 | IncludesDir string 19 | 20 | // The name used for layout templates :- templates that wrap other contents. 21 | // Defaults to "layouts". 22 | Layout string 23 | } 24 | 25 | // LoadTemplates loads templates from an fs.FS with a given config 26 | func (a *App) LoadTemplates(fs fs.FS, config *TemplateConfig) (err error) { 27 | 28 | if a.templates == nil { 29 | a.templates = make(map[string]*template.Template) 30 | } 31 | 32 | var tmpst *tempest.Tempest 33 | if config != nil { 34 | tmpst = tempest.WithConfig(&tempest.Config{ 35 | Layout: config.Layout, 36 | IncludesDir: config.IncludesDir, 37 | Ext: config.Ext, 38 | }) 39 | } else { 40 | tmpst = tempest.New() 41 | } 42 | 43 | a.templates, err = tmpst.LoadFS(fs) 44 | return err 45 | } 46 | -------------------------------------------------------------------------------- /render_test.go: -------------------------------------------------------------------------------- 1 | package coco 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func TestLoadTemplates(t *testing.T) { 11 | // Create a temporary directory for the templates 12 | tmpDir, err := os.MkdirTemp("", "coco-templates") 13 | if err != nil { 14 | t.Fatalf("Error creating temporary directory: %v", err) 15 | } 16 | defer os.RemoveAll(tmpDir) 17 | 18 | // Create template files with content 19 | templateFiles := map[string]string{ 20 | "template1.html": "Template 1 Content", 21 | "template2.html": "Template 2 Content", 22 | "template3.html": "Template 3 Content", 23 | } 24 | 25 | // Write template content to the files 26 | for filename, content := range templateFiles { 27 | templatePath := filepath.Join(tmpDir, filename) 28 | if err := os.WriteFile(templatePath, []byte(content), 0644); err != nil { 29 | t.Fatalf("Error writing template file %s: %v", filename, err) 30 | } 31 | } 32 | 33 | app := NewApp() 34 | config := &TemplateConfig{ 35 | Ext: ".html", 36 | IncludesDir: "includes", 37 | Layout: "layouts", 38 | } 39 | 40 | // Load templates with custom configuration 41 | err = app.LoadTemplates(NewTestFS(tmpDir), config) 42 | if err != nil { 43 | t.Errorf("Expected no error, but got: %v", err) 44 | } 45 | 46 | if app.templates == nil { 47 | t.Fatal("templates map was not initialized") 48 | } 49 | 50 | // Check if default values are used 51 | if app.templates["template1"] == nil { 52 | t.Errorf("Expected template1.html to be loaded, but it was not") 53 | } 54 | 55 | if app.templates["template2"] == nil { 56 | t.Errorf("Expected template2.html to be loaded, but it was not") 57 | } 58 | 59 | if app.templates["template3"] == nil { 60 | t.Errorf("Expected template3.html to be loaded, but it was not") 61 | } 62 | 63 | } 64 | 65 | // NewTestFS creates a fs.FS from the given directory path. 66 | func NewTestFS(dirPath string) fs.FS { 67 | return os.DirFS(dirPath) 68 | } 69 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package coco 2 | 3 | import ( 4 | coreContext "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "mime" 10 | "net/http" 11 | "net/url" 12 | "path/filepath" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/go-http-utils/fresh" 17 | "github.com/julienschmidt/httprouter" 18 | ) 19 | 20 | // Error is an error type that is returned when an error occurs while handling a request. 21 | type Error struct { 22 | Code int 23 | Message string 24 | } 25 | 26 | func (e Error) Error() string { 27 | return e.Message 28 | } 29 | 30 | // Range represents a byte range. 31 | type Range struct { 32 | Start int64 33 | End int64 34 | } 35 | 36 | type Request struct { 37 | r *http.Request 38 | 39 | BaseURL string 40 | 41 | // HostName contains the hostname derived from the Host HTTP header. 42 | HostName string 43 | 44 | // Ip contains the remote IP address of the request. 45 | Ip string 46 | 47 | // Ips contains the remote IP addresses from the X-Forwarded-For header. 48 | Ips []string 49 | 50 | // Protocol contains the request protocol string: "http" or "https" 51 | Protocol string 52 | 53 | // Secure is a boolean that is true if the request protocol is "https" 54 | Secure bool 55 | 56 | // Subdomains is a slice of subdomain strings. 57 | Subdomains []string 58 | 59 | // Xhr is a boolean that is true if the request's X-Requested-With header 60 | // field is "XMLHttpRequest". 61 | Xhr bool 62 | 63 | // OriginalURL is the original URL requested by the client. 64 | OriginalURL *url.URL 65 | 66 | // Cookies contains the cookies sent by the request. 67 | Cookies map[string]string 68 | 69 | // Body contains the body of the request. 70 | Body 71 | 72 | // Query contains the parsed query string from the URL. 73 | Query map[string]string 74 | 75 | // Params contains the Route parameters. 76 | Params map[string]string 77 | 78 | // SignedCookies contains the signed cookies sent by the request. 79 | SignedCookies map[string]string 80 | 81 | // Stale is a boolean that is true if the request is stale, false otherwise. 82 | Stale bool 83 | 84 | // Fresh is a boolean that is true if the request is fresh, false otherwise. 85 | Fresh bool 86 | 87 | // Method contains a string corresponding to the HTTP method of the request: 88 | // GET, POST, PUT, and so on. 89 | Method string 90 | 91 | // Path contains a string corresponding to the path of the request. 92 | Path string 93 | } 94 | 95 | type Body struct { 96 | req *http.Request 97 | } 98 | 99 | func newRequest(r *http.Request, w http.ResponseWriter, params httprouter.Params, app *App) (*Request, error) { 100 | hostName, err := parseHostName(r.Host) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | ip, err := parseIP(r.RemoteAddr) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | xhr := isXhr(r.Header.Get("X-Requested-With")) 111 | 112 | domainOffset := app.settings["subdomain offset"].(int) 113 | 114 | req := &Request{ 115 | BaseURL: filepath.Dir(r.URL.Path), 116 | HostName: hostName, 117 | Ip: ip, 118 | Ips: []string{}, 119 | Protocol: r.Proto, 120 | Secure: r.TLS != nil, 121 | Xhr: xhr, 122 | OriginalURL: r.URL, 123 | Cookies: parseCookies(r.Cookies()), 124 | Query: parseQuery(r.URL.Query()), 125 | Params: parseParams(params), 126 | Method: r.Method, 127 | Body: Body{r}, 128 | r: r, 129 | Path: r.URL.Path, 130 | Stale: !checkFreshness(r, w), 131 | Fresh: checkFreshness(r, w), 132 | Subdomains: parseSubdomains(hostName, domainOffset), 133 | } 134 | 135 | if app.IsTrustProxyEnabled() { 136 | req.Ips = parseXForwardedFor(r.Header.Get("X-Forwarded-For")) 137 | } 138 | 139 | return req, nil 140 | } 141 | 142 | // parseSubdomains parses the subdomains of the request hostname based on a subdomain offset. 143 | func parseSubdomains(host string, subdomainOffset int) []string { 144 | parts := strings.Split(host, ".") 145 | 146 | if len(parts) <= subdomainOffset { 147 | return nil 148 | } 149 | 150 | return parts[:len(parts)-subdomainOffset] 151 | } 152 | 153 | // parseXForwardedFor parses the X-Forwarded-For header to extract IP addresses 154 | func parseXForwardedFor(header string) []string { 155 | parts := strings.Split(header, ",") 156 | for i, part := range parts { 157 | parts[i] = strings.TrimSpace(part) 158 | } 159 | return parts 160 | } 161 | 162 | func parseHostName(host string) (string, error) { 163 | if idx := strings.Index(host, ":"); idx != -1 { 164 | return host[:idx], nil 165 | } 166 | return host, nil 167 | } 168 | 169 | func parseIP(remoteAddr string) (string, error) { 170 | if idx := strings.LastIndex(remoteAddr, ":"); idx != -1 { 171 | return remoteAddr[:idx], nil 172 | } 173 | return remoteAddr, nil 174 | } 175 | 176 | func isXhr(xRequestedWith string) bool { 177 | return strings.EqualFold(xRequestedWith, "XMLHttpRequest") 178 | } 179 | 180 | func parseCookies(cookies []*http.Cookie) map[string]string { 181 | cookieMap := make(map[string]string) 182 | for _, cookie := range cookies { 183 | cookieMap[cookie.Name] = cookie.Value 184 | } 185 | return cookieMap 186 | } 187 | 188 | func parseQuery(query url.Values) map[string]string { 189 | queryMap := make(map[string]string) 190 | for key, values := range query { 191 | queryMap[key] = values[0] // Assuming there's at least one value 192 | } 193 | return queryMap 194 | } 195 | 196 | func parseParams(params httprouter.Params) map[string]string { 197 | paramMap := make(map[string]string) 198 | for _, param := range params { 199 | paramMap[param.Key] = param.Value 200 | } 201 | return paramMap 202 | } 203 | 204 | func checkFreshness(req *http.Request, w http.ResponseWriter) bool { 205 | if req.Method != "GET" && req.Method != "HEAD" { 206 | return false 207 | } 208 | return fresh.IsFresh(req.Header, w.Header()) 209 | } 210 | 211 | // JSONError is an error type that is returned when the request body is not a valid JSON. 212 | type JSONError struct { 213 | Status int 214 | Message string 215 | } 216 | 217 | func (e JSONError) Error() string { 218 | return fmt.Sprintf("JSON error (Status %d): %s", e.Status, e.Message) 219 | } 220 | 221 | // JSON marshals the request body into the given interface. 222 | // It returns an error if the request body is not a valid JSON or if the 223 | // given interface is not a pointer. 224 | func (body *Body) JSON(dest interface{}) error { 225 | if dest == nil { 226 | return JSONError{http.StatusBadRequest, "Destination interface is nil"} 227 | } 228 | 229 | contentType := body.req.Header.Get("Content-Type") 230 | if !strings.HasPrefix(contentType, "application/json") { 231 | return JSONError{http.StatusUnsupportedMediaType, "Unsupported media type, expected 'application/json'"} 232 | } 233 | 234 | bdy, err := io.ReadAll(body.req.Body) 235 | if err != nil { 236 | return JSONError{http.StatusBadRequest, "Error reading JSON payload: " + err.Error()} 237 | } 238 | 239 | if err := body.req.Body.Close(); err != nil { 240 | return JSONError{http.StatusInternalServerError, "Error closing request body: " + err.Error()} 241 | } 242 | 243 | if err := json.Unmarshal(bdy, dest); err != nil { 244 | return JSONError{http.StatusBadRequest, "Error unmarshalling JSON: " + err.Error()} 245 | } 246 | 247 | return nil 248 | } 249 | 250 | // Text returns the request body as a string. 251 | func (body *Body) Text() (string, error) { 252 | b, err := io.ReadAll(body.req.Body) 253 | if err != nil { 254 | return "", fmt.Errorf("error reading text payload: %w", err) 255 | } 256 | 257 | if err := body.req.Body.Close(); err != nil { 258 | return "", fmt.Errorf("error closing request body: %w", err) 259 | } 260 | 261 | return string(b), nil 262 | } 263 | 264 | // FormData returns the body form data, expects request sent with `x-www-form-urlencoded` header or 265 | func (body *Body) FormData() (map[string][]string, error) { 266 | if body.req.Body == nil { 267 | return nil, errors.New("request body is nil") 268 | } 269 | defer body.req.Body.Close() 270 | 271 | contentType, _, err := mime.ParseMediaType(body.req.Header.Get("Content-Type")) 272 | if err != nil { 273 | return nil, fmt.Errorf("failed to parse Content-Type header: %w", err) 274 | } 275 | 276 | if contentType != "application/x-www-form-urlencoded" { 277 | return nil, fmt.Errorf("unsupported Content-Type: %s", contentType) 278 | } 279 | 280 | if err := body.req.ParseForm(); err != nil { 281 | return nil, fmt.Errorf("failed to parse form data: %w", err) 282 | } 283 | 284 | data := make(map[string][]string) 285 | for key, values := range body.req.Form { 286 | data[key] = make([]string, len(values)) 287 | copy(data[key], values) 288 | } 289 | 290 | return data, nil 291 | } 292 | 293 | func (a *App) IsTrustProxyEnabled() bool { 294 | return a.settings["trust proxy"].(bool) 295 | } 296 | 297 | func (req *Request) Cookie(name string) (value string, exists bool) { 298 | value, exists = req.Cookies[name] 299 | return value, exists 300 | } 301 | 302 | // GetParam returns the value of param `name` when present or `defaultValue`. 303 | func (req *Request) GetParam(name string) string { 304 | if value, ok := req.Params[name]; ok { 305 | return value 306 | } 307 | return "" 308 | } 309 | 310 | // Get returns the value of specified HTTP request header field (case-insensitive match) 311 | func (req *Request) Get(key string) string { 312 | if value := req.r.Header.Get(key); value != "" { 313 | return value 314 | } 315 | return "" 316 | } 317 | 318 | // Set sets the value of specified HTTP request header field (case-insensitive match) 319 | func (req *Request) Set(key string, value string) { 320 | req.r.Header.Set(key, value) 321 | } 322 | 323 | // QueryParam returns the value of query parameter `name` when present or `defaultValue`. 324 | func (req *Request) QueryParam(name string) string { 325 | if value, ok := req.Query[name]; ok { 326 | return value 327 | } 328 | return "" 329 | } 330 | 331 | // Is returns true if the incoming request’s “Content-Type” HTTP header field 332 | // matches the given mime type. 333 | func (req *Request) Is(mime string) bool { 334 | contentType := req.r.Header.Get("Content-Type") 335 | if contentType == "" { 336 | return false 337 | } 338 | 339 | switch mime { 340 | case "json": 341 | mime = "application/json" 342 | case "html": 343 | mime = "text/html" 344 | case "xml": 345 | mime = "application/xml" 346 | case "text": 347 | mime = "text/plain" 348 | } 349 | 350 | mimeParts := strings.Split(mime, "/") 351 | ctParts := strings.Split(contentType, "/") 352 | 353 | if mimeParts[1] == "*" { 354 | return strings.EqualFold(mimeParts[0], ctParts[0]) 355 | } 356 | 357 | return strings.EqualFold(mime, contentType) 358 | } 359 | 360 | // Range returns the first range found in the request’s “Range” header field. 361 | // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range 362 | func (req *Request) Range(size int64) ([]Range, error) { 363 | rangeHeader := req.r.Header.Get("Range") 364 | if rangeHeader == "" { 365 | return nil, nil // No Range header is not an error, just no range requested 366 | } 367 | 368 | if !strings.HasPrefix(rangeHeader, "bytes=") { 369 | return nil, fmt.Errorf("invalid range specifier") 370 | } 371 | 372 | rangesStr := strings.Split(rangeHeader[6:], ",") 373 | var ranges []Range 374 | for _, rStr := range rangesStr { 375 | rStr = strings.TrimSpace(rStr) 376 | if rStr == "" { 377 | continue 378 | } 379 | 380 | rangeParts := strings.SplitN(rStr, "-", 2) 381 | if len(rangeParts) != 2 { 382 | return nil, fmt.Errorf("invalid range format") 383 | } 384 | 385 | startStr, endStr := strings.TrimSpace(rangeParts[0]), strings.TrimSpace(rangeParts[1]) 386 | var start, end int64 387 | var err error 388 | 389 | if startStr != "" { 390 | start, err = strconv.ParseInt(startStr, 10, 64) 391 | if err != nil { 392 | return nil, fmt.Errorf("invalid range start value") 393 | } 394 | } 395 | 396 | if endStr != "" { 397 | end, err = strconv.ParseInt(endStr, 10, 64) 398 | if err != nil { 399 | return nil, fmt.Errorf("invalid range end value") 400 | } 401 | } 402 | 403 | if startStr == "" { 404 | start = size - end 405 | end = size - 1 406 | } else if endStr == "" { 407 | end = size - 1 408 | } 409 | 410 | if start > end || start < 0 || end >= size { 411 | continue 412 | } 413 | 414 | ranges = append(ranges, Range{Start: start, End: end}) 415 | } 416 | 417 | if len(ranges) == 0 { 418 | return nil, fmt.Errorf("no satisfiable range found") 419 | } 420 | 421 | return ranges, nil 422 | } 423 | 424 | func (req *Request) Context() coreContext.Context { 425 | return req.r.Context() 426 | } 427 | 428 | func (req *Request) SetContext(ctx coreContext.Context) { 429 | req.r = req.r.WithContext(ctx) 430 | } 431 | 432 | //// Accepts checks if the specified mine types are acceptable, based on the request’s Accept HTTP header field. 433 | //// The method returns the best match, or if none of the specified mine types is acceptable, returns "". 434 | //func (req *Request) Accepts(mime ...string) string { 435 | // 436 | // if len(mime) == 0 { 437 | // return "" 438 | // } 439 | // 440 | // return "" 441 | //} 442 | 443 | //// AcceptsCharsets returns true if the incoming request’s “Accept-Charset” HTTP header field 444 | //// includes the given charset. 445 | //func (req *Request) AcceptsCharsets(charset string) bool { 446 | // return false 447 | //} 448 | // 449 | //// AcceptsEncodings returns true if the incoming request’s “Accept-Encoding” HTTP header field 450 | //// includes the given encoding. 451 | //func (req *Request) AcceptsEncodings(encoding string) bool { 452 | // return false 453 | //} 454 | 455 | //// AcceptsLanguages returns true if the incoming request’s “Accept-Language” HTTP header field 456 | //// includes the given language. 457 | //func (req *Request) AcceptsLanguages(lang string) bool { 458 | // return false 459 | //} 460 | -------------------------------------------------------------------------------- /request_test.go: -------------------------------------------------------------------------------- 1 | package coco 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/http/httptest" 10 | "net/url" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/julienschmidt/httprouter" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestJSONError_Error(t *testing.T) { 19 | err := Error{ 20 | Code: 400, 21 | Message: "Bad Request", 22 | } 23 | 24 | assert.Equal(t, "Bad Request", err.Error()) 25 | } 26 | 27 | func TestBody_JSON(t *testing.T) { 28 | t.Run("it should unmarshal JSON successfully", func(t *testing.T) { 29 | body := &Body{ 30 | req: &http.Request{ 31 | Header: http.Header{ 32 | "Content-Type": []string{"application/json"}, 33 | }, 34 | Body: io.NopCloser(bytes.NewBufferString(`{"key": "value"}`)), // Valid JSON 35 | }, 36 | } 37 | 38 | var data map[string]string 39 | err := body.JSON(&data) 40 | if err != nil { 41 | t.Fatalf("Expected no error, got %v", err) 42 | } 43 | if data["key"] != "value" { 44 | t.Errorf("Expected key to be 'value', got '%s'", data["key"]) 45 | } 46 | }) 47 | 48 | t.Run("it should return error for nil destination", func(t *testing.T) { 49 | body := &Body{ 50 | req: &http.Request{ 51 | Header: http.Header{ 52 | "Content-Type": []string{"application/json"}, 53 | }, 54 | Body: io.NopCloser(bytes.NewBufferString(`{"key": "value"}`)), 55 | }, 56 | } 57 | 58 | err := body.JSON(nil) 59 | if err == nil { 60 | t.Fatalf("Expected error for nil destination, got nil") 61 | } 62 | 63 | var jsonErr JSONError 64 | if !errors.As(err, &jsonErr) || jsonErr.Status != http.StatusBadRequest { 65 | t.Fatalf("Expected JSONError with StatusBadRequest, got %v", err) 66 | } 67 | }) 68 | 69 | t.Run("it should return error for unsupported media type", func(t *testing.T) { 70 | body := &Body{ 71 | req: &http.Request{ 72 | Header: http.Header{ 73 | "Content-Type": []string{"text/plain"}, 74 | }, 75 | Body: io.NopCloser(bytes.NewBufferString(`{"key": "value"}`)), 76 | }, 77 | } 78 | 79 | var data map[string]string 80 | err := body.JSON(&data) 81 | if err == nil { 82 | t.Fatalf("Expected error for unsupported media type, got nil") 83 | } 84 | 85 | var jsonErr JSONError 86 | if !errors.As(err, &jsonErr) || jsonErr.Status != http.StatusUnsupportedMediaType { 87 | t.Fatalf("Expected JSONError with StatusUnsupportedMediaType, got %v", err) 88 | } 89 | }) 90 | 91 | t.Run("it should return error for invalid JSON", func(t *testing.T) { 92 | body := &Body{ 93 | req: &http.Request{ 94 | Header: http.Header{ 95 | "Content-Type": []string{"application/json"}, 96 | }, 97 | Body: io.NopCloser(bytes.NewBufferString(`{"key": "value`)), // Invalid JSON 98 | }, 99 | } 100 | 101 | var data map[string]string 102 | err := body.JSON(&data) 103 | if err == nil { 104 | t.Fatalf("Expected error for invalid JSON, got nil") 105 | } 106 | 107 | var jsonErr JSONError 108 | if !errors.As(err, &jsonErr) || jsonErr.Status != http.StatusBadRequest { 109 | t.Fatalf("Expected JSONError with StatusBadRequest, got %v", err) 110 | } 111 | }) 112 | 113 | t.Run("it should return error when reading body fails", func(t *testing.T) { 114 | body := &Body{ 115 | req: &http.Request{ 116 | Header: http.Header{ 117 | "Content-Type": []string{"application/json"}, 118 | }, 119 | Body: io.NopCloser(errReader{}), 120 | }, 121 | } 122 | 123 | var data map[string]string 124 | err := body.JSON(&data) 125 | if err == nil { 126 | t.Fatalf("Expected error when reading body fails, got nil") 127 | } 128 | 129 | var jsonErr JSONError 130 | if !errors.As(err, &jsonErr) || jsonErr.Status != http.StatusBadRequest { 131 | t.Fatalf("Expected JSONError with StatusInternalServerError, got %v", err) 132 | } 133 | }) 134 | } 135 | 136 | func TestBody_Text(t *testing.T) { 137 | t.Run("it should return body as string", func(t *testing.T) { 138 | body := &Body{ 139 | req: &http.Request{ 140 | Body: io.NopCloser(bytes.NewBufferString("test body content")), 141 | }, 142 | } 143 | 144 | text, err := body.Text() 145 | if err != nil { 146 | t.Fatalf("Expected no error, got %v", err) 147 | } 148 | if text != "test body content" { 149 | t.Errorf("Expected body to be 'test body content', got '%s'", text) 150 | } 151 | }) 152 | 153 | t.Run("it should return error when closing body fails", func(t *testing.T) { 154 | body := &Body{ 155 | req: &http.Request{ 156 | Body: &badCloser{Reader: bytes.NewBufferString("test body content")}, 157 | }, 158 | } 159 | 160 | _, err := body.Text() 161 | if err == nil { 162 | t.Fatalf("Expected error when closing body, got nil") 163 | } 164 | if !strings.Contains(err.Error(), "error closing request body") { 165 | t.Errorf("Expected error closing request body, got %v", err) 166 | } 167 | }) 168 | 169 | t.Run("it should return error when reading body fails", func(t *testing.T) { 170 | body := &Body{ 171 | req: &http.Request{ 172 | Body: io.NopCloser(errReader{}), 173 | }, 174 | } 175 | 176 | _, err := body.Text() 177 | if err == nil { 178 | t.Fatalf("Expected error when reading body, got nil") 179 | } 180 | if !strings.Contains(err.Error(), "error reading text payload") { 181 | t.Errorf("Expected error reading text payload, got %v", err) 182 | } 183 | }) 184 | } 185 | 186 | func TestBody_FormData(t *testing.T) { 187 | t.Run("it should handle FormData successfully", func(t *testing.T) { 188 | app := NewApp() 189 | req := httptest.NewRequest("POST", "/", strings.NewReader("key1=value1&key2=value2")) 190 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 191 | 192 | w := httptest.NewRecorder() 193 | 194 | request, _ := newRequest(req, w, nil, app) 195 | 196 | body := request.Body 197 | 198 | data, err := body.FormData() 199 | if err != nil { 200 | t.Fatalf("Expected no error, got %v", err) 201 | } 202 | 203 | if len(data) != 2 || data["key1"][0] != "value1" || data["key2"][0] != "value2" { 204 | t.Errorf("FormData did not parse correctly: got %+v", data) 205 | } 206 | }) 207 | 208 | t.Run("it should return error for unsupported Content-Type", func(t *testing.T) { 209 | body := &Body{ 210 | req: &http.Request{ 211 | Header: http.Header{ 212 | "Content-Type": []string{"text/plain"}, 213 | }, 214 | Body: io.NopCloser(strings.NewReader("key1=value1&key2=value2")), 215 | }, 216 | } 217 | 218 | _, err := body.FormData() 219 | if err == nil { 220 | t.Fatalf("Expected error for unsupported Content-Type, got nil") 221 | } 222 | 223 | if !strings.Contains(err.Error(), "unsupported Content-Type: text/plain") { 224 | t.Errorf("Expected specific error message for unsupported Content-Type, got %v", err) 225 | } 226 | }) 227 | 228 | t.Run("it should return error when reading body fails", func(t *testing.T) { 229 | app := NewApp() 230 | req := httptest.NewRequest("POST", "/", io.NopCloser(errReader{})) 231 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 232 | 233 | w := httptest.NewRecorder() 234 | 235 | request, _ := newRequest(req, w, nil, app) 236 | 237 | body := request.Body 238 | _, err := body.FormData() 239 | if err == nil { 240 | t.Fatalf("Expected error when reading body, got nil") 241 | } 242 | 243 | if !strings.Contains(err.Error(), "failed to parse form data") { 244 | t.Errorf("Expected specific error message when reading body fails, got %v", err) 245 | } 246 | }) 247 | 248 | t.Run("it should return error when parsing form fails", func(t *testing.T) { 249 | app := NewApp() 250 | 251 | req := httptest.NewRequest("POST", "/", strings.NewReader("%")) 252 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 253 | 254 | w := httptest.NewRecorder() 255 | 256 | request, _ := newRequest(req, w, nil, app) 257 | 258 | body := request.Body 259 | 260 | _, err := body.FormData() 261 | if err == nil { 262 | t.Fatalf("Expected error when parsing form, got nil") 263 | } 264 | 265 | if !strings.Contains(err.Error(), "failed to parse form data") { 266 | t.Errorf("Expected specific error message when parsing form fails, got %v", err) 267 | } 268 | }) 269 | } 270 | 271 | func TestRequest_Cookie(t *testing.T) { 272 | t.Run("it should retrieve a cookie if it exists", func(t *testing.T) { 273 | app := NewApp() 274 | req := httptest.NewRequest("GET", "/", nil) 275 | w := httptest.NewRecorder() 276 | req.AddCookie(&http.Cookie{Name: "session_token", Value: "abc123"}) 277 | request, _ := newRequest(req, w, nil, app) 278 | 279 | cookie, ok := request.Cookie("session_token") 280 | 281 | // Assertion 282 | if !ok { 283 | t.Fatal("Expected cookie to exist") 284 | } 285 | if cookie != "abc123" { 286 | t.Errorf("Expected cookie value to be 'abc123', got '%s'", cookie) 287 | } 288 | }) 289 | 290 | t.Run("it should return an error if the cookie does not exist", func(t *testing.T) { 291 | // Setup 292 | req := &http.Request{Header: http.Header{"Cookie": []string{"session_token=abc123"}}} 293 | request := &Request{r: req} 294 | 295 | // Execution 296 | _, ok := request.Cookie("nonexistent_cookie") 297 | if ok { 298 | t.Fatal("Expected error for nonexistent cookie, got nil") 299 | } 300 | }) 301 | } 302 | 303 | func TestRequest_Get(t *testing.T) { 304 | t.Run("it should retrieve the param value if it exists", func(t *testing.T) { 305 | app := NewApp() 306 | req := httptest.NewRequest("GET", "/?name=test", nil) 307 | req.Header.Add("X-Custom-Header", "value123") 308 | w := httptest.NewRecorder() 309 | 310 | params := httprouter.Params{ 311 | httprouter.Param{Key: "id", Value: "123"}, 312 | } 313 | 314 | request, _ := newRequest(req, w, params, app) 315 | 316 | value := request.Get("X-Custom-Header") 317 | 318 | if value != "value123" { 319 | t.Errorf("Expected header value to be 'value123', got '%s'", value) 320 | } 321 | 322 | param := request.GetParam("id") 323 | 324 | if param != "123" { 325 | t.Errorf("Expected param value to be '123', got '%s'", param) 326 | } 327 | 328 | query := request.QueryParam("name") 329 | 330 | if query != "test" { 331 | t.Errorf("Expected query value to be 'test', got '%s'", query) 332 | } 333 | 334 | }) 335 | } 336 | 337 | func TestRequest_Is(t *testing.T) { 338 | t.Run("it should return true if the content type matches", func(t *testing.T) { 339 | app := NewApp() 340 | req := httptest.NewRequest("GET", "/", nil) 341 | req.Header.Add("Content-Type", "application/json") 342 | w := httptest.NewRecorder() 343 | 344 | request, _ := newRequest(req, w, nil, app) 345 | 346 | isJSON := request.Is("application/json") 347 | 348 | // Assertion 349 | if !isJSON { 350 | t.Error("Expected content type to be JSON") 351 | } 352 | }) 353 | 354 | t.Run("it should return false if the content type does not match", func(t *testing.T) { 355 | app := NewApp() 356 | req := httptest.NewRequest("GET", "/", nil) 357 | req.Header.Add("X-Custom-Header", "value123") 358 | w := httptest.NewRecorder() 359 | 360 | params := httprouter.Params{ 361 | httprouter.Param{Key: "id", Value: "123"}, 362 | } 363 | 364 | request, _ := newRequest(req, w, params, app) 365 | 366 | isJSON := request.Is("application/json") 367 | 368 | if isJSON { 369 | t.Error("Expected content type not to be JSON") 370 | } 371 | }) 372 | } 373 | 374 | func TestRequest_Range(t *testing.T) { 375 | t.Run("it should return nil if no Range header is present", func(t *testing.T) { 376 | req := &http.Request{Header: http.Header{}} 377 | request := &Request{r: req} 378 | 379 | ranges, err := request.Range(1000) 380 | if err != nil { 381 | t.Fatalf("Expected no error, got %v", err) 382 | } 383 | if ranges != nil { 384 | t.Errorf("Expected nil ranges, got %v", ranges) 385 | } 386 | }) 387 | 388 | t.Run("it should return error for invalid range specifier", func(t *testing.T) { 389 | req := &http.Request{Header: http.Header{"Range": []string{"invalid=0-499"}}} 390 | request := &Request{r: req} 391 | 392 | _, err := request.Range(1000) 393 | if err == nil { 394 | t.Fatal("Expected an error for invalid range specifier, got nil") 395 | } 396 | }) 397 | 398 | t.Run("it should return error for invalid range format", func(t *testing.T) { 399 | req := &http.Request{Header: http.Header{"Range": []string{"bytes=500"}}} 400 | request := &Request{r: req} 401 | 402 | _, err := request.Range(1000) 403 | if err == nil { 404 | t.Fatal("Expected an error for invalid range format, got nil") 405 | } 406 | }) 407 | 408 | t.Run("it should return error for invalid range start value", func(t *testing.T) { 409 | req := &http.Request{Header: http.Header{"Range": []string{"bytes=abc-500"}}} 410 | request := &Request{r: req} 411 | 412 | _, err := request.Range(1000) 413 | if err == nil { 414 | t.Fatal("Expected an error for invalid range start value, got nil") 415 | } 416 | }) 417 | 418 | t.Run("it should return error for invalid range end value", func(t *testing.T) { 419 | req := &http.Request{Header: http.Header{"Range": []string{"bytes=0-xyz"}}} 420 | request := &Request{r: req} 421 | 422 | _, err := request.Range(1000) 423 | if err == nil { 424 | t.Fatal("Expected an error for invalid range end value, got nil") 425 | } 426 | }) 427 | 428 | t.Run("it should return error for unsatisfiable range", func(t *testing.T) { 429 | req := &http.Request{Header: http.Header{"Range": []string{"bytes=1000-2000"}}} 430 | request := &Request{r: req} 431 | 432 | _, err := request.Range(500) 433 | if err == nil { 434 | t.Fatal("Expected an error for unsatisfiable range, got nil") 435 | } 436 | }) 437 | 438 | t.Run("it should return satisfiable range", func(t *testing.T) { 439 | req := &http.Request{Header: http.Header{"Range": []string{"bytes=0-499"}}} 440 | request := &Request{r: req} 441 | 442 | ranges, err := request.Range(1000) 443 | if err != nil { 444 | t.Fatalf("Expected no error, got %v", err) 445 | } 446 | if len(ranges) != 1 || ranges[0].Start != 0 || ranges[0].End != 499 { 447 | t.Errorf("Expected range [0-499], got %v", ranges) 448 | } 449 | }) 450 | 451 | t.Run("it should handle multiple ranges, including unsatisfiable ones", func(t *testing.T) { 452 | req := &http.Request{Header: http.Header{"Range": []string{"bytes=0-499,1000-1500"}}} 453 | request := &Request{r: req} 454 | 455 | ranges, err := request.Range(1000) 456 | if err != nil { 457 | t.Fatalf("Expected no error, got %v", err) 458 | } 459 | if len(ranges) != 1 || ranges[0].Start != 0 || ranges[0].End != 499 { 460 | t.Errorf("Expected only satisfiable range [0-499], got %v", ranges) 461 | } 462 | }) 463 | } 464 | 465 | func TestRequest_Context(t *testing.T) { 466 | t.Run("it should return the request's context", func(t *testing.T) { 467 | // Setup 468 | req := &http.Request{Header: http.Header{}} 469 | request := &Request{r: req} 470 | 471 | // Execution 472 | ctx := request.Context() 473 | 474 | // Assertion 475 | if ctx == nil { 476 | t.Fatal("Expected context to be non-nil") 477 | } 478 | }) 479 | } 480 | 481 | func Test_newRequest(t *testing.T) { 482 | // Create a new App instance 483 | app := NewApp() 484 | 485 | validRequest := httptest.NewRequest("GET", "http://example.com/path", nil) 486 | validRequest.RemoteAddr = "192.0.2.1:1234" 487 | // Define test cases 488 | tests := []struct { 489 | name string 490 | expectedResult func() *Request 491 | expectError bool 492 | }{ 493 | { 494 | name: "valid request", 495 | expectedResult: func() *Request { 496 | return &Request{ 497 | BaseURL: "/", 498 | HostName: "example.com", 499 | Ip: "192.0.2.1", 500 | Protocol: "HTTP/1.1", 501 | Secure: false, 502 | Xhr: false, 503 | OriginalURL: &url.URL{Scheme: "http", Host: "example.com", Path: "/path"}, 504 | Method: "GET", 505 | Path: "/path", 506 | Fresh: false, 507 | Stale: true, 508 | r: validRequest, 509 | Body: Body{req: validRequest}, 510 | } 511 | }, 512 | expectError: false, 513 | }, 514 | } 515 | 516 | for _, tc := range tests { 517 | t.Run(tc.name, func(t *testing.T) { 518 | 519 | w := httptest.NewRecorder() 520 | params := httprouter.Params{} 521 | 522 | got, err := newRequest(validRequest, w, params, app) 523 | if (err != nil) != tc.expectError { 524 | t.Fatalf("newRequest() error = %v, wantErr %v", err, tc.expectError) 525 | } 526 | if err != nil { 527 | return 528 | } 529 | 530 | expected := tc.expectedResult() 531 | 532 | // Compare the expected and got Request struct fields individually 533 | if got.BaseURL != expected.BaseURL || 534 | got.HostName != expected.HostName || 535 | got.Ip != expected.Ip || 536 | got.Protocol != expected.Protocol || 537 | got.Secure != expected.Secure || 538 | got.Xhr != expected.Xhr || 539 | got.Method != expected.Method || 540 | got.Path != expected.Path || 541 | got.Fresh != expected.Fresh || 542 | got.Stale != expected.Stale { 543 | t.Errorf("newRequest() fields do not match the expected result") 544 | } 545 | 546 | // Compare the OriginalURL separately 547 | if got.OriginalURL.String() != expected.OriginalURL.String() { 548 | t.Errorf("newRequest() OriginalURL got = %v, want %v", got.OriginalURL, expected.OriginalURL) 549 | } 550 | }) 551 | } 552 | } 553 | 554 | type errReader struct{} 555 | 556 | func (e errReader) Read(p []byte) (n int, err error) { 557 | return 0, fmt.Errorf("simulated read error") 558 | } 559 | 560 | type badCloser struct { 561 | Reader *bytes.Buffer 562 | } 563 | 564 | func (bc *badCloser) Read(p []byte) (n int, err error) { 565 | return bc.Reader.Read(p) 566 | } 567 | 568 | func (bc *badCloser) Close() error { 569 | return errors.New("error closing request body") 570 | } 571 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package coco 2 | 3 | import ( 4 | "bufio" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/base64" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "log" 12 | "mime" 13 | "net" 14 | "net/http" 15 | "net/url" 16 | "os" 17 | "path/filepath" 18 | "strings" 19 | "time" 20 | ) 21 | 22 | type DownloadOption struct { 23 | MaxAge int 24 | LastModified bool 25 | Headers map[string]string 26 | Dotfiles string 27 | AcceptRanges bool 28 | CacheControl bool 29 | Immutable bool 30 | } 31 | 32 | var ( 33 | defaultDownloadOptions = &DownloadOption{ 34 | MaxAge: 0, 35 | LastModified: false, 36 | Headers: nil, 37 | Dotfiles: "ignore", 38 | AcceptRanges: true, 39 | CacheControl: true, 40 | Immutable: false, 41 | } 42 | ErrDirPath = errors.New("specified path is a directory") 43 | ErrDotfilesDeny = errors.New("serving dotfiles is not allowed") 44 | ) 45 | 46 | // func (r *Response) setContentType(contentType string) { 47 | // if cty := r.ww.Header().Get("Content-Type"); cty == "" { 48 | // r.ww.Header().Set("Content-Type", contentType) 49 | // } 50 | // } 51 | 52 | type wrappedWriter struct { 53 | http.ResponseWriter 54 | statusCode int 55 | statusCodeWritten bool 56 | hijacker http.Hijacker 57 | flusher http.Flusher 58 | } 59 | 60 | func wrapWriter(original http.ResponseWriter) *wrappedWriter { 61 | w := &wrappedWriter{ResponseWriter: original} 62 | if h, ok := original.(http.Hijacker); ok { 63 | w.hijacker = h 64 | } 65 | if f, ok := original.(http.Flusher); ok { 66 | w.flusher = f 67 | } 68 | return w 69 | } 70 | 71 | func (w *wrappedWriter) WriteHeader(code int) { 72 | if !w.statusCodeWritten { 73 | w.statusCodeWritten = true 74 | w.statusCode = code 75 | w.ResponseWriter.WriteHeader(code) 76 | } 77 | } 78 | 79 | func (w *wrappedWriter) Write(b []byte) (int, error) { 80 | if !w.statusCodeWritten { 81 | if w.statusCode == 0 { 82 | w.statusCode = http.StatusOK 83 | } 84 | w.WriteHeader(w.statusCode) 85 | } 86 | return w.ResponseWriter.Write(b) 87 | } 88 | 89 | func (w *wrappedWriter) _statusCode() int { 90 | return w.statusCode 91 | } 92 | 93 | func (w *wrappedWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 94 | 95 | if w.hijacker == nil { 96 | return nil, nil, http.ErrHijacked 97 | } 98 | return w.hijacker.Hijack() 99 | } 100 | 101 | func (w *wrappedWriter) Flush() { 102 | if w.flusher == nil { 103 | return 104 | } 105 | w.flusher.Flush() 106 | } 107 | 108 | type Response struct { 109 | ww *wrappedWriter 110 | ctx *context 111 | } 112 | 113 | // Append sets the specified value to the HTTP response header field. 114 | // If the header is not already set, it creates the header with the specified value. 115 | func (r *Response) Append(key, value string) *Response { 116 | r.ww.Header().Add(key, value) 117 | return r 118 | } 119 | 120 | // Attachment sets the Content-Disposition header to “attachment”. 121 | // If a filename is given, then the Content-Type header is set based on the filename’s extension. 122 | func (r *Response) Attachment(filename string) *Response { 123 | contentDisposition := "attachment" 124 | 125 | if filename != "" { 126 | cleanFilename := filepath.Clean(filename) 127 | escapedFilename := url.PathEscape(cleanFilename) 128 | contentDisposition = fmt.Sprintf("attachment; filename=%s", escapedFilename) 129 | ext := filepath.Ext(cleanFilename) 130 | mimeType := mime.TypeByExtension(ext) 131 | if mimeType == "" { 132 | mimeType = "application/octet-stream" 133 | } 134 | r.ww.Header().Set("Content-Type", mimeType) 135 | } 136 | 137 | r.ww.Header().Set("Content-Disposition", contentDisposition) 138 | return r 139 | } 140 | 141 | // Cookie sets cookie name to value 142 | func (r *Response) Cookie(cookie *http.Cookie) *Response { 143 | http.SetCookie(r.ww, cookie) 144 | return r 145 | } 146 | 147 | // SignedCookie SecureCookie sets a signed cookie 148 | func (r *Response) SignedCookie(cookie *http.Cookie, secret string) *Response { 149 | mac := hmac.New(sha256.New, []byte(secret)) 150 | _, _ = mac.Write([]byte(cookie.Value)) 151 | signature := mac.Sum(nil) 152 | encodedSignature := base64.StdEncoding.EncodeToString(signature) 153 | cookie.Value = fmt.Sprintf("%s.%s", encodedSignature, cookie.Value) 154 | http.SetCookie(r.ww, cookie) 155 | return r 156 | } 157 | 158 | // ClearCookie clears the cookie by setting the MaxAge to -1 159 | func (r *Response) ClearCookie(name string) *Response { 160 | cookie := &http.Cookie{Name: name, MaxAge: -1} 161 | http.SetCookie(r.ww, cookie) 162 | return r 163 | } 164 | 165 | // Download transfers the file at the given path. 166 | // Sets the Content-Type response HTTP header field based on the filename’s extension. 167 | func (r *Response) Download(filepath, filename string, options *DownloadOption, cb func(error)) { 168 | if options == nil { 169 | options = defaultDownloadOptions 170 | } 171 | 172 | if options.Dotfiles == "deny" && strings.HasPrefix(filename, ".") { 173 | cb(ErrDotfilesDeny) 174 | return 175 | } 176 | 177 | r.SendFile(filepath, filename, options, cb) 178 | } 179 | 180 | // SendFile transfers the file at the given path. 181 | // Sets the Content-Type response HTTP header field based on the filename’s extension. 182 | func (r *Response) SendFile(filePath, fileName string, options *DownloadOption, cb func(error)) { 183 | if cb == nil { 184 | cb = func(err error) { 185 | if err != nil { 186 | fmt.Printf("Error sending file: %v\n", err) 187 | } 188 | } 189 | } 190 | 191 | if options == nil { 192 | options = defaultDownloadOptions 193 | } 194 | 195 | fi, err := os.Stat(filePath) 196 | if err != nil { 197 | cb(err) 198 | return 199 | } 200 | 201 | if fi.IsDir() { 202 | cb(ErrDirPath) 203 | return 204 | } 205 | 206 | file, err := os.Open(filePath) 207 | if err != nil { 208 | cb(err) 209 | return 210 | } 211 | defer file.Close() 212 | 213 | ext := filepath.Ext(fileName) 214 | mimeType := mime.TypeByExtension(ext) 215 | if mimeType == "" { 216 | mimeType = "application/octet-stream" 217 | } 218 | r.ww.Header().Set("Content-Type", mimeType) 219 | 220 | if options.CacheControl { 221 | cacheControlValues := []string{"public"} 222 | if options.MaxAge > 0 { 223 | cacheControlValues = append(cacheControlValues, fmt.Sprintf("max-age=%d", options.MaxAge)) 224 | } else { 225 | cacheControlValues = append(cacheControlValues, "max-age=0") 226 | } 227 | if options.Immutable { 228 | cacheControlValues = append(cacheControlValues, "immutable") 229 | } 230 | r.ww.Header().Set("Cache-Control", strings.Join(cacheControlValues, ", ")) 231 | } 232 | 233 | if options.LastModified { 234 | r.ww.Header().Set("Last-Modified", fi.ModTime().Format(time.RFC1123)) 235 | } 236 | if options.AcceptRanges { 237 | r.ww.Header().Set("Accept-Ranges", "bytes") 238 | } 239 | for key, value := range options.Headers { 240 | r.ww.Header().Set(key, value) 241 | } 242 | 243 | encodedFilename := url.PathEscape(fileName) 244 | r.ww.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename*=UTF-8''%s", encodedFilename)) 245 | 246 | http.ServeContent(r.ww, r.ctx.request(), fileName, fi.ModTime(), file) 247 | 248 | cb(nil) 249 | } 250 | 251 | // JSON sends a JSON response with the given payload. 252 | func (r *Response) JSON(v interface{}) *Response { 253 | r.Set("Content-Type", "application/json; charset=utf-8") 254 | jsn, err := json.Marshal(v) 255 | if err != nil { 256 | http.Error(r.ww, err.Error(), http.StatusInternalServerError) 257 | return r 258 | } 259 | 260 | _, err = r.ww.Write(jsn) 261 | if err != nil { 262 | http.Error(r.ww, err.Error(), http.StatusInternalServerError) 263 | } 264 | 265 | return r 266 | } 267 | 268 | // Send sends the HTTP response. 269 | func (r *Response) Send(body interface{}) *Response { 270 | var data []byte 271 | var err error 272 | 273 | switch v := body.(type) { 274 | case string: 275 | r.Set("Content-Type", "text/plain; charset=utf-8") 276 | data = []byte(v) 277 | case []byte: 278 | data = v 279 | default: 280 | return r.JSON(v) 281 | } 282 | 283 | _, err = r.ww.Write(data) 284 | if err != nil { 285 | http.Error(r.ww, err.Error(), http.StatusInternalServerError) 286 | } 287 | 288 | return r 289 | } 290 | 291 | // Set sets the specified value to the HTTP response header field. 292 | func (r *Response) Set(key string, value string) *Response { 293 | key = http.CanonicalHeaderKey(key) 294 | if key == "Content-Type" && !strings.Contains(value, "charset") { 295 | if strings.HasPrefix(value, "text/") || strings.Contains(value, "application/json") { 296 | value += "; charset=utf-8" 297 | } 298 | } 299 | r.ww.Header().Set(key, value) 300 | return r 301 | } 302 | 303 | // SendStatus sends the HTTP response status code. 304 | func (r *Response) SendStatus(statusCode int) *Response { 305 | r.Set("Content-Type", "text/plain; charset=utf-8") 306 | r.ww.WriteHeader(statusCode) 307 | _, err := r.ww.Write([]byte(http.StatusText(statusCode))) 308 | if err != nil { 309 | http.Error(r.ww, err.Error(), http.StatusInternalServerError) 310 | } 311 | return r 312 | } 313 | 314 | // Status sets the HTTP status for the response. 315 | func (r *Response) Status(code int) *Response { 316 | r.ww.statusCode = code 317 | r.ww.statusCodeWritten = false 318 | return r 319 | } 320 | 321 | func (r *Response) StatusCode() int { 322 | return r.ww._statusCode() 323 | } 324 | 325 | // Get returns the HTTP response header specified by field. 326 | func (r *Response) Get(key string) string { 327 | return r.ww.Header().Get(key) 328 | } 329 | 330 | // Location sets the response Location HTTP header to the specified path parameter. 331 | func (r *Response) Location(path string) *Response { 332 | if path == "back" { 333 | path = r.ctx.request().Referer() 334 | } 335 | 336 | if path == "" { 337 | path = "/" 338 | } 339 | 340 | r.Set("Location", path) 341 | return r 342 | } 343 | 344 | // Redirect redirects to the URL derived from the specified path, with specified status. 345 | func (r *Response) Redirect(path string, status ...int) *Response { 346 | statusCode := http.StatusFound 347 | if len(status) > 0 { 348 | statusCode = status[0] 349 | } 350 | 351 | if path == "back" { 352 | path = r.ctx.request().Referer() 353 | } 354 | 355 | if path == "" { 356 | path = "/" 357 | } 358 | 359 | r.Location(path) 360 | r.Status(statusCode) 361 | r.Status(statusCode) 362 | r.ww.WriteHeader(statusCode) 363 | r.ww.Flush() 364 | return r 365 | } 366 | 367 | // Type sets the Content-Type HTTP header to the MIME type as determined by the filename’s extension. 368 | func (r *Response) Type(filename string) *Response { 369 | mimeType := mime.TypeByExtension(filepath.Ext(filename)) 370 | if mimeType == "" { 371 | mimeType = "application/octet-stream" 372 | } 373 | r.Set("Content-Type", mimeType) 374 | return r 375 | } 376 | 377 | // Vary adds a field to the Vary header, if it doesn't already exist. 378 | func (r *Response) Vary(field string) *Response { 379 | if field == "" { 380 | log.Println("field argument is required") 381 | return r 382 | } 383 | 384 | existingHeader := r.Get("Vary") 385 | if existingHeader == "*" || field == "*" { 386 | r.Set("Vary", "*") 387 | return r 388 | } 389 | 390 | fields := strings.Split(existingHeader, ",") 391 | for _, f := range fields { 392 | if strings.TrimSpace(f) == field { 393 | return r 394 | } 395 | } 396 | 397 | if existingHeader != "" { 398 | field = existingHeader + ", " + field 399 | } 400 | r.Set("Vary", field) 401 | return r 402 | } 403 | 404 | // Render renders a template with data and sends a text/html response. 405 | func (r *Response) Render(name string, data interface{}) *Response { 406 | tmpl, ok := r.ctx.templates[name] 407 | if !ok { 408 | http.Error(r.ww, fmt.Sprintf("template %s not found", name), http.StatusInternalServerError) 409 | return r 410 | } 411 | 412 | r.Set("Content-Type", "text/html; charset=utf-8") 413 | err := tmpl.Execute(r.ww, data) 414 | if err != nil { 415 | http.Error(r.ww, err.Error(), http.StatusInternalServerError) 416 | } 417 | return r 418 | } 419 | -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | package coco_test 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "errors" 8 | "io" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | "testing" 16 | 17 | "github.com/spf13/afero" 18 | "github.com/stretchr/testify/assert" 19 | "github.com/tobolabs/coco/v2" 20 | ) 21 | 22 | func TestResponseAppend(t *testing.T) { 23 | app := coco.NewApp() 24 | 25 | // Define a route that appends headers to the response 26 | app.Get("/test-append", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 27 | res.Append("X-Custom-Header", "Value1") 28 | res.Append("X-Custom-Header", "Value2") 29 | res.SendStatus(http.StatusOK) 30 | }) 31 | 32 | // Create a test server using the app's handler 33 | srv := httptest.NewServer(app) 34 | defer srv.Close() 35 | 36 | // Make a request to the test server 37 | resp, err := http.Get(srv.URL + "/test-append") 38 | if err != nil { 39 | t.Fatalf("Failed to make GET request: %v", err) 40 | } 41 | defer resp.Body.Close() 42 | 43 | // Check if the header is set correctly 44 | values, ok := resp.Header["X-Custom-Header"] 45 | if !ok { 46 | t.Fatalf("Header 'X-Custom-Header' not found") 47 | } 48 | if len(values) != 2 { 49 | t.Fatalf("Expected 2 values for 'X-Custom-Header', got %d", len(values)) 50 | } 51 | if values[0] != "Value1" { 52 | t.Errorf("Expected first value of 'X-Custom-Header' to be 'Value1', got '%s'", values[0]) 53 | } 54 | if values[1] != "Value2" { 55 | t.Errorf("Expected second value of 'X-Custom-Header' to be 'Value2', got '%s'", values[1]) 56 | } 57 | } 58 | 59 | func TestResponseAttachment(t *testing.T) { 60 | app := coco.NewApp() 61 | 62 | app.Get("/test-attachment", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 63 | res.Attachment("test.pdf") 64 | }) 65 | 66 | srv := httptest.NewServer(app) 67 | defer srv.Close() 68 | 69 | resp, err := http.Get(srv.URL + "/test-attachment") 70 | if err != nil { 71 | t.Fatalf("Failed to make GET request: %v", err) 72 | } 73 | defer resp.Body.Close() 74 | 75 | contentDisposition := resp.Header.Get("Content-Disposition") 76 | if contentDisposition != "attachment; filename=test.pdf" { 77 | t.Errorf("Expected Content-Disposition to be 'attachment; filename=test.pdf', got '%s'", contentDisposition) 78 | } 79 | 80 | contentType := resp.Header.Get("Content-Type") 81 | if contentType != "application/pdf" { 82 | t.Errorf("Expected Content-Type to be 'application/pdf', got '%s'", contentType) 83 | } 84 | } 85 | 86 | func TestResponseCookie(t *testing.T) { 87 | app := coco.NewApp() 88 | 89 | app.Get("/test-cookie", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 90 | cookie := &http.Cookie{Name: "test", Value: "cookie_value"} 91 | res.Cookie(cookie) 92 | }) 93 | 94 | srv := httptest.NewServer(app) 95 | defer srv.Close() 96 | 97 | resp, err := http.Get(srv.URL + "/test-cookie") 98 | if err != nil { 99 | t.Fatalf("Failed to make GET request: %v", err) 100 | } 101 | defer resp.Body.Close() 102 | 103 | cookies := resp.Cookies() 104 | var cookie *http.Cookie 105 | for _, c := range cookies { 106 | if c.Name == "test" { 107 | cookie = c 108 | break 109 | } 110 | } 111 | 112 | if cookie == nil { 113 | t.Fatalf("Cookie 'test' not found") 114 | } 115 | 116 | if cookie.Value != "cookie_value" { 117 | t.Errorf("Expected cookie 'test' to have value 'cookie_value', got '%s'", cookie.Value) 118 | } 119 | } 120 | 121 | func TestResponseSignedCookie(t *testing.T) { 122 | app := coco.NewApp() 123 | secret := "secret_key" 124 | 125 | app.Get("/test-signed-cookie", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 126 | cookie := &http.Cookie{Name: "signed_test", Value: "signed_value"} 127 | res.SignedCookie(cookie, secret) 128 | }) 129 | 130 | srv := httptest.NewServer(app) 131 | defer srv.Close() 132 | 133 | resp, err := http.Get(srv.URL + "/test-signed-cookie") 134 | if err != nil { 135 | t.Fatalf("Failed to make GET request: %v", err) 136 | } 137 | defer resp.Body.Close() 138 | 139 | cookies := resp.Cookies() 140 | var cookie *http.Cookie 141 | for _, c := range cookies { 142 | if c.Name == "signed_test" { 143 | cookie = c 144 | break 145 | } 146 | } 147 | 148 | if cookie == nil { 149 | t.Fatalf("Signed cookie 'signed_test' not found") 150 | } 151 | 152 | // Verify the signature 153 | parts := strings.Split(cookie.Value, ".") 154 | if len(parts) != 2 { 155 | t.Fatalf("Expected signed cookie value to have two parts, got %d", len(parts)) 156 | } 157 | 158 | signature, err := base64.StdEncoding.DecodeString(parts[0]) 159 | if err != nil { 160 | t.Fatalf("Error decoding signature: %v", err) 161 | } 162 | 163 | mac := hmac.New(sha256.New, []byte(secret)) 164 | _, _ = mac.Write([]byte(parts[1])) // The original value 165 | expectedMAC := mac.Sum(nil) 166 | 167 | if !hmac.Equal(signature, expectedMAC) { 168 | t.Errorf("Signature does not match") 169 | } 170 | } 171 | 172 | func TestResponseClearCookie(t *testing.T) { 173 | app := coco.NewApp() 174 | 175 | app.Get("/clear-cookie", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 176 | // Assuming ClearCookie is implemented to take the name of the cookie 177 | res.ClearCookie("test") 178 | }) 179 | 180 | srv := httptest.NewServer(app) 181 | defer srv.Close() 182 | 183 | resp, err := http.Get(srv.URL + "/clear-cookie") 184 | if err != nil { 185 | t.Fatalf("Failed to make GET request: %v", err) 186 | } 187 | defer resp.Body.Close() 188 | 189 | // Check if the 'Set-Cookie' header is present and contains 'Max-Age=-1' 190 | cookies := resp.Cookies() 191 | found := false 192 | for _, cookie := range cookies { 193 | if cookie.Name == "test" && cookie.MaxAge == -1 { 194 | found = true 195 | break 196 | } 197 | } 198 | 199 | if !found { 200 | t.Errorf("Expected cookie 'test' to be cleared with 'Max-Age=-1'") 201 | } 202 | } 203 | 204 | func TestDownload(t *testing.T) { 205 | app := coco.NewApp() 206 | 207 | tmpFile, err := os.CreateTemp("", "testfile-*.txt") 208 | if err != nil { 209 | t.Fatalf("Failed to create temp file: %v", err) 210 | } 211 | defer os.Remove(tmpFile.Name()) // Clean up after the test. 212 | 213 | fileName := filepath.Base(tmpFile.Name()) 214 | filePath := tmpFile.Name() 215 | 216 | // Define test cases as a slice of anonymous structs. 217 | tests := []struct { 218 | name string 219 | path string 220 | filename string 221 | options *coco.DownloadOption 222 | wantErr bool 223 | errChecker func(error) bool 224 | }{ 225 | { 226 | name: "DefaultOptions", 227 | path: filePath, 228 | filename: fileName, 229 | options: nil, 230 | wantErr: false, 231 | }, 232 | { 233 | name: "DenyDotfiles", 234 | path: "/path/to/.dotfile", 235 | filename: ".dotfile", 236 | options: &coco.DownloadOption{ 237 | Dotfiles: "deny", 238 | }, 239 | wantErr: true, 240 | errChecker: func(err error) bool { return errors.Is(err, coco.ErrDotfilesDeny) }, 241 | }, 242 | { 243 | name: "CustomOptions", 244 | path: filePath, 245 | filename: fileName, 246 | options: &coco.DownloadOption{ 247 | MaxAge: 3600, 248 | Headers: map[string]string{ 249 | "X-Custom-Header": "CustomValue", 250 | }, 251 | }, 252 | wantErr: false, 253 | }, 254 | { 255 | name: "ErrorHandling", 256 | path: "/path/to/non-existent/file.txt", 257 | filename: "file.txt", 258 | options: nil, 259 | wantErr: true, 260 | }, 261 | } 262 | 263 | // Iterate over each test case. 264 | for _, tc := range tests { 265 | t.Run(tc.name, func(t *testing.T) { 266 | app.Get("/download-"+tc.name, func(res coco.Response, req *coco.Request, next coco.NextFunc) { 267 | res.Download(tc.path, tc.filename, tc.options, func(err error) { 268 | if (err != nil) != tc.wantErr { 269 | t.Errorf("Download() error = %v, wantErr %v", err, tc.wantErr) 270 | } 271 | if tc.wantErr && tc.errChecker != nil && !tc.errChecker(err) { 272 | t.Errorf("Download() error = %v, does not match expected error", err) 273 | } 274 | }) 275 | }) 276 | 277 | srv := httptest.NewServer(app) 278 | defer srv.Close() 279 | 280 | resp, err := http.Get(srv.URL + "/download-" + tc.name) 281 | if err != nil { 282 | t.Fatalf("Failed to make GET request: %v", err) 283 | } 284 | defer resp.Body.Close() 285 | 286 | // Additional checks can be added here to verify headers and response content. 287 | }) 288 | } 289 | } 290 | 291 | func TestSendFile(t *testing.T) { 292 | 293 | type handlerSetupResult struct { 294 | routePath string 295 | tempFilePath string 296 | cleanup func() 297 | } 298 | // Define a struct for test cases 299 | type testCase struct { 300 | name string 301 | setupHandler func(*testing.T, *coco.App) handlerSetupResult 302 | validate func(*testing.T, *http.Response, handlerSetupResult) 303 | expectError bool 304 | } 305 | 306 | // Define the test cases 307 | testCases := []testCase{ 308 | { 309 | name: "NormalOperation", 310 | setupHandler: func(t *testing.T, app *coco.App) handlerSetupResult { 311 | 312 | tmpFile, err := os.CreateTemp("", "testfile-*.txt") 313 | if err != nil { 314 | t.Fatalf("Failed to create temp file: %v", err) 315 | } 316 | 317 | fileName := filepath.Base(tmpFile.Name()) 318 | filePath := tmpFile.Name() 319 | 320 | app.Get("/send-file", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 321 | options := &coco.DownloadOption{ 322 | MaxAge: 3600, 323 | LastModified: true, 324 | Headers: map[string]string{ 325 | "X-Custom-Header": "TestValue", 326 | }, 327 | AcceptRanges: true, 328 | CacheControl: true, 329 | Immutable: true, 330 | } 331 | res.SendFile(filePath, fileName, options, nil) 332 | }) 333 | 334 | return handlerSetupResult{ 335 | routePath: "/send-file", 336 | tempFilePath: fileName, 337 | cleanup: func() { os.Remove(tmpFile.Name()) }, 338 | } 339 | }, 340 | validate: func(t *testing.T, resp *http.Response, setup handlerSetupResult) { 341 | expectedMimeType := "text/plain; charset=utf-8" 342 | if resp.Header.Get("Content-Type") != expectedMimeType { 343 | t.Errorf("Expected Content-Type '%s', got '%s'", expectedMimeType, resp.Header.Get("Content-Type")) 344 | } 345 | 346 | // Test Cache-Control header 347 | expectedCacheControl := "public, max-age=3600, immutable" 348 | if resp.Header.Get("Cache-Control") != expectedCacheControl { 349 | t.Errorf("Expected Cache-Control '%s', got '%s'", expectedCacheControl, resp.Header.Get("Cache-Control")) 350 | } 351 | // Test Last-Modified header 352 | if resp.Header.Get("Last-Modified") == "" { 353 | t.Error("Expected Last-Modified header to be set") 354 | } 355 | 356 | // Test Accept-Ranges header 357 | if resp.Header.Get("Accept-Ranges") != "bytes" { 358 | t.Error("Expected Accept-Ranges header to be 'bytes'") 359 | } 360 | 361 | // Test custom header 362 | if resp.Header.Get("X-Custom-Header") != "TestValue" { 363 | t.Errorf("Expected custom header 'X-Custom-Header' to be 'TestValue'") 364 | } 365 | 366 | // Test Content-Disposition header 367 | expectedContentDisposition := "attachment; filename*=UTF-8''" + url.PathEscape(setup.tempFilePath) 368 | if resp.Header.Get("Content-Disposition") != expectedContentDisposition { 369 | t.Errorf("Expected Content-Disposition '%s', got '%s'", expectedContentDisposition, resp.Header.Get("Content-Disposition")) 370 | } 371 | }, 372 | expectError: false, 373 | }, 374 | { 375 | name: "NilCallback", 376 | setupHandler: func(t *testing.T, app *coco.App) handlerSetupResult { 377 | return handlerSetupResult{ 378 | routePath: "/send-file-nil-callback", 379 | cleanup: func() {}, 380 | } 381 | }, 382 | validate: func(t *testing.T, resp *http.Response, setup handlerSetupResult) {}, 383 | expectError: false, 384 | }, 385 | { 386 | name: "StatError", 387 | setupHandler: func(t *testing.T, app *coco.App) handlerSetupResult { 388 | nonExistentFilePath := "/path/to/non-existent/file.txt" 389 | app.Get("/send-file-stat-error", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 390 | res.SendFile(nonExistentFilePath, "file.txt", nil, func(err error) { 391 | if err == nil { 392 | t.Error("Expected an error when stating a non-existent file path, got nil") 393 | } 394 | }) 395 | }) 396 | return handlerSetupResult{ 397 | routePath: "/send-file-stat-error", 398 | cleanup: func() {}, 399 | } 400 | }, 401 | validate: func(t *testing.T, resp *http.Response, setup handlerSetupResult) {}, 402 | expectError: false, 403 | }, 404 | { 405 | name: "DirectoryError", 406 | setupHandler: func(t *testing.T, app *coco.App) handlerSetupResult { 407 | dirPath := os.TempDir() 408 | app.Get("/send-file-dir-error", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 409 | res.SendFile(dirPath, "dir", nil, func(err error) { 410 | if err == nil { 411 | t.Error("Expected an error when sending a directory, got nil") 412 | } 413 | }) 414 | }) 415 | return handlerSetupResult{ 416 | routePath: "/send-file-dir-error", 417 | cleanup: func() {}, 418 | } 419 | }, 420 | validate: func(t *testing.T, resp *http.Response, setup handlerSetupResult) { 421 | 422 | }, 423 | }, 424 | { 425 | name: "OpenError", 426 | setupHandler: func(t *testing.T, app *coco.App) handlerSetupResult { 427 | tmpFile, err := os.CreateTemp("", "testfile-*.txt") 428 | if err != nil { 429 | t.Fatalf("Failed to create temp file: %v", err) 430 | } 431 | tmpFile.Close() 432 | os.Remove(tmpFile.Name()) // Delete the file to induce an open error 433 | 434 | fileName := filepath.Base(tmpFile.Name()) 435 | filePath := tmpFile.Name() 436 | 437 | app.Get("/send-file-open-error", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 438 | res.SendFile(filePath, fileName, nil, func(err error) { 439 | if err == nil { 440 | t.Error("Expected an error when opening a non-existent file, got nil") 441 | } 442 | }) 443 | }) 444 | return handlerSetupResult{ 445 | routePath: "/send-file-open-error", 446 | tempFilePath: filePath, 447 | cleanup: func() {}, 448 | } 449 | }, 450 | validate: func(t *testing.T, resp *http.Response, setup handlerSetupResult) { 451 | }, 452 | expectError: false, 453 | }, 454 | } 455 | 456 | for _, tc := range testCases { 457 | t.Run(tc.name, func(t *testing.T) { 458 | app := coco.NewApp() 459 | 460 | setup := tc.setupHandler(t, app) 461 | 462 | defer setup.cleanup() 463 | srv := httptest.NewServer(app) 464 | defer srv.Close() 465 | resp, err := http.Get(srv.URL + setup.routePath) 466 | if err != nil { 467 | t.Fatalf("Failed to make GET request: %v", err) 468 | } 469 | defer resp.Body.Close() 470 | 471 | if tc.validate != nil { 472 | tc.validate(t, resp, setup) 473 | } 474 | 475 | if tc.expectError && err == nil { 476 | t.Errorf("Expected error, got nil") 477 | } else if !tc.expectError && err != nil { 478 | t.Errorf("Expected no error, got %v", err) 479 | } 480 | }) 481 | } 482 | } 483 | 484 | func TestResponseJSON(t *testing.T) { 485 | 486 | app := coco.NewApp() 487 | 488 | app.Get("/test-json", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 489 | res.JSON(map[string]string{"status": "ok"}) 490 | }) 491 | 492 | app.Get("/json-status", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 493 | res.Status(http.StatusCreated).JSON(map[string]string{"status": "ok"}) 494 | }) 495 | 496 | srv := httptest.NewServer(app) 497 | defer srv.Close() 498 | 499 | resp, err := http.Get(srv.URL + "/test-json") 500 | if err != nil { 501 | t.Fatalf("Failed to make GET request: %v", err) 502 | } 503 | defer resp.Body.Close() 504 | 505 | if !strings.Contains(resp.Header.Get("Content-Type"), "application/json") { 506 | t.Errorf("Expected Content-Type to be 'application/json', got '%s'", resp.Header.Get("Content-Type")) 507 | } 508 | 509 | resp, err = http.Get(srv.URL + "/json-status") 510 | if err != nil { 511 | t.Fatalf("Failed to make GET request: %v", err) 512 | } 513 | 514 | if resp.StatusCode != http.StatusCreated { 515 | t.Errorf("Expected status code to be %d, got %d", http.StatusCreated, resp.StatusCode) 516 | } 517 | 518 | defer resp.Body.Close() 519 | 520 | if !strings.Contains(resp.Header.Get("Content-Type"), "application/json") { 521 | t.Fatalf("Expected Content-Type to be 'application/json', got '%s'", resp.Header.Get("Content-Type")) 522 | } 523 | } 524 | 525 | func TestResponseSend(t *testing.T) { 526 | app := coco.NewApp() 527 | 528 | app.Get("/test-send", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 529 | res.Send("Hello World") 530 | }) 531 | 532 | srv := httptest.NewServer(app) 533 | defer srv.Close() 534 | 535 | resp, err := http.Get(srv.URL + "/test-send") 536 | if err != nil { 537 | t.Fatalf("Failed to make GET request: %v", err) 538 | } 539 | defer resp.Body.Close() 540 | 541 | if resp.Header.Get("Content-Type") != "text/plain; charset=utf-8" { 542 | t.Errorf("Expected Content-Type to be 'text/plain; charset=utf-8', got '%s'", resp.Header.Get("Content-Type")) 543 | } 544 | 545 | body, err := io.ReadAll(resp.Body) 546 | if err != nil { 547 | t.Fatalf("Failed to read response body: %v", err) 548 | } 549 | 550 | if string(body) != "Hello World" { 551 | t.Errorf("Expected response body to be 'Hello World', got '%s'", string(body)) 552 | } 553 | } 554 | 555 | func TestResponseSet(t *testing.T) { 556 | 557 | app := coco.NewApp() 558 | 559 | app.Get("/test-set", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 560 | res.Set("X-Custom-Header", "TestValue") 561 | res.Send("Hello World") 562 | }) 563 | 564 | srv := httptest.NewServer(app) 565 | defer srv.Close() 566 | 567 | resp, err := http.Get(srv.URL + "/test-set") 568 | if err != nil { 569 | t.Fatalf("Failed to make GET request: %v", err) 570 | } 571 | defer resp.Body.Close() 572 | 573 | if resp.Header.Get("X-Custom-Header") != "TestValue" { 574 | t.Errorf("Expected X-Custom-Header to be 'TestValue', got '%s'", resp.Header.Get("X-Custom-Header")) 575 | } 576 | } 577 | 578 | func TestResponseSendStatus(t *testing.T) { 579 | app := coco.NewApp() 580 | 581 | app.Get("/test-send-status", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 582 | res.SendStatus(http.StatusNotFound) 583 | }) 584 | 585 | srv := httptest.NewServer(app) 586 | defer srv.Close() 587 | 588 | resp, err := http.Get(srv.URL + "/test-send-status") 589 | if err != nil { 590 | t.Fatalf("Failed to make GET request: %v", err) 591 | } 592 | defer resp.Body.Close() 593 | 594 | if resp.StatusCode != http.StatusNotFound { 595 | t.Errorf("Expected status code to be %d, got %d", http.StatusNotFound, resp.StatusCode) 596 | } 597 | } 598 | 599 | func TestResponseStatus(t *testing.T) { 600 | app := coco.NewApp() 601 | 602 | app.Get("/test-response-status", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 603 | res.Status(http.StatusNotFound).Send("Not Found") 604 | }) 605 | 606 | srv := httptest.NewServer(app) 607 | defer srv.Close() 608 | 609 | resp, err := http.Get(srv.URL + "/test-response-status") 610 | if err != nil { 611 | t.Fatalf("Failed to make GET request: %v", err) 612 | } 613 | defer resp.Body.Close() 614 | 615 | if resp.StatusCode != http.StatusNotFound { 616 | t.Errorf("Expected status code to be %d, got %d", http.StatusNotFound, resp.StatusCode) 617 | } 618 | } 619 | 620 | func TestResponseType(t *testing.T) { 621 | app := coco.NewApp() 622 | 623 | app.Get("/test-response-type", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 624 | res.Type("text/plain").Send("Hello World") 625 | }) 626 | 627 | srv := httptest.NewServer(app) 628 | defer srv.Close() 629 | 630 | resp, err := http.Get(srv.URL + "/test-response-type") 631 | if err != nil { 632 | t.Fatalf("Failed to make GET request: %v", err) 633 | } 634 | defer resp.Body.Close() 635 | 636 | expectedContentType := "text/plain; charset=utf-8" 637 | if resp.Header.Get("Content-Type") != expectedContentType { 638 | t.Errorf("Expected Content-Type to be '%s', got '%s'", expectedContentType, resp.Header.Get("Content-Type")) 639 | } 640 | } 641 | 642 | func TestResponseVary(t *testing.T) { 643 | app := coco.NewApp() 644 | 645 | app.Get("/test-response-vary", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 646 | res.Vary("Origin") 647 | res.Send("Hello World") 648 | }) 649 | 650 | srv := httptest.NewServer(app) 651 | defer srv.Close() 652 | 653 | resp, err := http.Get(srv.URL + "/test-response-vary") 654 | if err != nil { 655 | t.Fatalf("Failed to make GET request: %v", err) 656 | } 657 | defer resp.Body.Close() 658 | 659 | if resp.Header.Get("Vary") != "Origin" { 660 | t.Errorf("Expected Vary to be 'Origin', got '%s'", resp.Header.Get("Vary")) 661 | } 662 | } 663 | 664 | func TestResponseGet(t *testing.T) { 665 | app := coco.NewApp() 666 | 667 | app.Get("/test-response-get", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 668 | res.Set("X-Custom-Header", "TestValue") 669 | 670 | if res.Get("X-Custom-Header") != "TestValue" { 671 | t.Errorf("Expected X-Custom-Header to be 'TestValue', got '%s'", res.Get("X-Custom-Header")) 672 | } 673 | res.SendStatus(http.StatusOK) 674 | }) 675 | 676 | srv := httptest.NewServer(app) 677 | defer srv.Close() 678 | 679 | _, err := http.Get(srv.URL + "/test-response-get") 680 | if err != nil { 681 | t.Fatalf("Failed to make GET request: %v", err) 682 | } 683 | 684 | } 685 | 686 | func TestResponseRedirect(t *testing.T) { 687 | app := coco.NewApp() 688 | 689 | app.Get("/test-redirect", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 690 | res.Status(http.StatusCreated).Send("Redirect successful") 691 | }) 692 | 693 | app.Get("/test-response-redirect", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 694 | res.Redirect("/test-redirect") 695 | }) 696 | 697 | srv := httptest.NewServer(app) 698 | defer srv.Close() 699 | 700 | resp, err := http.Get(srv.URL + "/test-response-redirect") 701 | if err != nil { 702 | t.Fatalf("Failed to make GET request: %v", err) 703 | } 704 | defer resp.Body.Close() 705 | 706 | if resp.StatusCode != http.StatusCreated { 707 | t.Errorf("Expected status code to be %d, got %d", http.StatusFound, resp.StatusCode) 708 | } 709 | } 710 | 711 | func TestResponseLocation(t *testing.T) { 712 | app := coco.NewApp() 713 | 714 | app.Get("/test-response-location", func(res coco.Response, req *coco.Request, next coco.NextFunc) { 715 | res.Location("/test-location") 716 | res.SendStatus(http.StatusOK) 717 | }) 718 | 719 | srv := httptest.NewServer(app) 720 | defer srv.Close() 721 | 722 | resp, err := http.Get(srv.URL + "/test-response-location") 723 | if err != nil { 724 | t.Fatalf("Failed to make GET request: %v", err) 725 | } 726 | defer resp.Body.Close() 727 | 728 | location, err := resp.Location() 729 | if err != nil { 730 | t.Fatalf("Failed to get Location header: %v", err) 731 | } 732 | 733 | if location.Path != "/test-location" { 734 | t.Errorf("Expected Location header to be '/test-location', got '%s'", location.Path) 735 | } 736 | } 737 | 738 | func TestResponseRender(t *testing.T) { 739 | appFS := afero.NewMemMapFs() 740 | 741 | htmlTemplate := `