code.gitea.io/gitea@v1.22.3/services/webhook/deliver_test.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package webhook
     5  
     6  import (
     7  	"context"
     8  	"io"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"net/url"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"code.gitea.io/gitea/models/db"
    17  	"code.gitea.io/gitea/models/unittest"
    18  	webhook_model "code.gitea.io/gitea/models/webhook"
    19  	"code.gitea.io/gitea/modules/hostmatcher"
    20  	"code.gitea.io/gitea/modules/setting"
    21  	"code.gitea.io/gitea/modules/util"
    22  	webhook_module "code.gitea.io/gitea/modules/webhook"
    23  
    24  	"github.com/stretchr/testify/assert"
    25  	"github.com/stretchr/testify/require"
    26  )
    27  
    28  func TestWebhookProxy(t *testing.T) {
    29  	oldWebhook := setting.Webhook
    30  	t.Cleanup(func() {
    31  		setting.Webhook = oldWebhook
    32  	})
    33  
    34  	setting.Webhook.ProxyURL = "http://localhost:8080"
    35  	setting.Webhook.ProxyURLFixed, _ = url.Parse(setting.Webhook.ProxyURL)
    36  	setting.Webhook.ProxyHosts = []string{"*.discordapp.com", "discordapp.com"}
    37  
    38  	allowedHostMatcher := hostmatcher.ParseHostMatchList("webhook.ALLOWED_HOST_LIST", "discordapp.com,s.discordapp.com")
    39  
    40  	tests := []struct {
    41  		req     string
    42  		want    string
    43  		wantErr bool
    44  	}{
    45  		{
    46  			req:     "https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx",
    47  			want:    "http://localhost:8080",
    48  			wantErr: false,
    49  		},
    50  		{
    51  			req:     "http://s.discordapp.com/assets/xxxxxx",
    52  			want:    "http://localhost:8080",
    53  			wantErr: false,
    54  		},
    55  		{
    56  			req:     "http://github.com/a/b",
    57  			want:    "",
    58  			wantErr: false,
    59  		},
    60  		{
    61  			req:     "http://www.discordapp.com/assets/xxxxxx",
    62  			want:    "",
    63  			wantErr: true,
    64  		},
    65  	}
    66  	for _, tt := range tests {
    67  		t.Run(tt.req, func(t *testing.T) {
    68  			req, err := http.NewRequest("POST", tt.req, nil)
    69  			require.NoError(t, err)
    70  
    71  			u, err := webhookProxy(allowedHostMatcher)(req)
    72  			if tt.wantErr {
    73  				assert.Error(t, err)
    74  				return
    75  			}
    76  
    77  			assert.NoError(t, err)
    78  
    79  			got := ""
    80  			if u != nil {
    81  				got = u.String()
    82  			}
    83  			assert.Equal(t, tt.want, got)
    84  		})
    85  	}
    86  }
    87  
    88  func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
    89  	assert.NoError(t, unittest.PrepareTestDatabase())
    90  
    91  	done := make(chan struct{}, 1)
    92  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    93  		assert.Equal(t, "/webhook", r.URL.Path)
    94  		assert.Equal(t, "Bearer s3cr3t-t0ken", r.Header.Get("Authorization"))
    95  		w.WriteHeader(200)
    96  		done <- struct{}{}
    97  	}))
    98  	t.Cleanup(s.Close)
    99  
   100  	hook := &webhook_model.Webhook{
   101  		RepoID:      3,
   102  		URL:         s.URL + "/webhook",
   103  		ContentType: webhook_model.ContentTypeJSON,
   104  		IsActive:    true,
   105  		Type:        webhook_module.GITEA,
   106  	}
   107  	err := hook.SetHeaderAuthorization("Bearer s3cr3t-t0ken")
   108  	assert.NoError(t, err)
   109  	assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
   110  
   111  	hookTask := &webhook_model.HookTask{
   112  		HookID:         hook.ID,
   113  		EventType:      webhook_module.HookEventPush,
   114  		PayloadVersion: 2,
   115  	}
   116  
   117  	hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask)
   118  	assert.NoError(t, err)
   119  	assert.NotNil(t, hookTask)
   120  
   121  	assert.NoError(t, Deliver(context.Background(), hookTask))
   122  	select {
   123  	case <-done:
   124  	case <-time.After(5 * time.Second):
   125  		t.Fatal("waited to long for request to happen")
   126  	}
   127  
   128  	assert.True(t, hookTask.IsSucceed)
   129  	assert.Equal(t, "******", hookTask.RequestInfo.Headers["Authorization"])
   130  }
   131  
   132  func TestWebhookDeliverHookTask(t *testing.T) {
   133  	assert.NoError(t, unittest.PrepareTestDatabase())
   134  
   135  	done := make(chan struct{}, 1)
   136  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   137  		assert.Equal(t, "PUT", r.Method)
   138  		switch r.URL.Path {
   139  		case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98":
   140  			// Version 1
   141  			assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
   142  			assert.Equal(t, "", r.Header.Get("Content-Type"))
   143  			body, err := io.ReadAll(r.Body)
   144  			assert.NoError(t, err)
   145  			assert.Equal(t, `{"data": 42}`, string(body))
   146  
   147  		case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51":
   148  			// Version 2
   149  			assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
   150  			assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
   151  			body, err := io.ReadAll(r.Body)
   152  			assert.NoError(t, err)
   153  			assert.Len(t, body, 2147)
   154  
   155  		default:
   156  			w.WriteHeader(404)
   157  			t.Fatalf("unexpected url path %s", r.URL.Path)
   158  			return
   159  		}
   160  		w.WriteHeader(200)
   161  		done <- struct{}{}
   162  	}))
   163  	t.Cleanup(s.Close)
   164  
   165  	hook := &webhook_model.Webhook{
   166  		RepoID:      3,
   167  		IsActive:    true,
   168  		Type:        webhook_module.MATRIX,
   169  		URL:         s.URL + "/webhook",
   170  		HTTPMethod:  "PUT",
   171  		ContentType: webhook_model.ContentTypeJSON,
   172  		Meta:        `{"message_type":0}`, // text
   173  	}
   174  	assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
   175  
   176  	t.Run("Version 1", func(t *testing.T) {
   177  		hookTask := &webhook_model.HookTask{
   178  			HookID:         hook.ID,
   179  			EventType:      webhook_module.HookEventPush,
   180  			PayloadContent: `{"data": 42}`,
   181  			PayloadVersion: 1,
   182  		}
   183  
   184  		hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask)
   185  		assert.NoError(t, err)
   186  		assert.NotNil(t, hookTask)
   187  
   188  		assert.NoError(t, Deliver(context.Background(), hookTask))
   189  		select {
   190  		case <-done:
   191  		case <-time.After(5 * time.Second):
   192  			t.Fatal("waited to long for request to happen")
   193  		}
   194  
   195  		assert.True(t, hookTask.IsSucceed)
   196  	})
   197  
   198  	t.Run("Version 2", func(t *testing.T) {
   199  		p := pushTestPayload()
   200  		data, err := p.JSONPayload()
   201  		assert.NoError(t, err)
   202  
   203  		hookTask := &webhook_model.HookTask{
   204  			HookID:         hook.ID,
   205  			EventType:      webhook_module.HookEventPush,
   206  			PayloadContent: string(data),
   207  			PayloadVersion: 2,
   208  		}
   209  
   210  		hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask)
   211  		assert.NoError(t, err)
   212  		assert.NotNil(t, hookTask)
   213  
   214  		assert.NoError(t, Deliver(context.Background(), hookTask))
   215  		select {
   216  		case <-done:
   217  		case <-time.After(5 * time.Second):
   218  			t.Fatal("waited to long for request to happen")
   219  		}
   220  
   221  		assert.True(t, hookTask.IsSucceed)
   222  	})
   223  }
   224  
   225  func TestWebhookDeliverSpecificTypes(t *testing.T) {
   226  	assert.NoError(t, unittest.PrepareTestDatabase())
   227  
   228  	type hookCase struct {
   229  		gotBody    chan []byte
   230  		httpMethod string // default to POST
   231  	}
   232  
   233  	cases := map[string]*hookCase{
   234  		webhook_module.SLACK:      {},
   235  		webhook_module.DISCORD:    {},
   236  		webhook_module.DINGTALK:   {},
   237  		webhook_module.TELEGRAM:   {},
   238  		webhook_module.MSTEAMS:    {},
   239  		webhook_module.FEISHU:     {},
   240  		webhook_module.MATRIX:     {httpMethod: "PUT"},
   241  		webhook_module.WECHATWORK: {},
   242  		webhook_module.PACKAGIST:  {},
   243  	}
   244  
   245  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   246  		typ := strings.Split(r.URL.Path, "/")[1] // URL: "/{webhook_type}/other-path"
   247  		assert.Equal(t, "application/json", r.Header.Get("Content-Type"), r.URL.Path)
   248  		assert.Equal(t, util.IfZero(cases[typ].httpMethod, "POST"), r.Method, "webhook test request %q", r.URL.Path)
   249  		body, _ := io.ReadAll(r.Body) // read request and send it back to the test by testcase's chan
   250  		cases[typ].gotBody <- body
   251  		w.WriteHeader(http.StatusNoContent)
   252  	}))
   253  	t.Cleanup(s.Close)
   254  
   255  	p := pushTestPayload()
   256  	data, err := p.JSONPayload()
   257  	assert.NoError(t, err)
   258  
   259  	for typ := range cases {
   260  		cases[typ].gotBody = make(chan []byte, 1)
   261  		t.Run(typ, func(t *testing.T) {
   262  			t.Parallel()
   263  			hook := &webhook_model.Webhook{
   264  				RepoID:   3,
   265  				IsActive: true,
   266  				Type:     typ,
   267  				URL:      s.URL + "/" + typ,
   268  				Meta:     "{}",
   269  			}
   270  			assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
   271  
   272  			hookTask := &webhook_model.HookTask{
   273  				HookID:         hook.ID,
   274  				EventType:      webhook_module.HookEventPush,
   275  				PayloadContent: string(data),
   276  				PayloadVersion: 2,
   277  			}
   278  
   279  			hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask)
   280  			assert.NoError(t, err)
   281  			assert.NotNil(t, hookTask)
   282  
   283  			assert.NoError(t, Deliver(context.Background(), hookTask))
   284  
   285  			select {
   286  			case gotBody := <-cases[typ].gotBody:
   287  				assert.NotEqual(t, string(data), string(gotBody), "request body must be different from the event payload")
   288  				assert.Equal(t, hookTask.RequestInfo.Body, string(gotBody), "delivered webhook payload doesn't match saved request")
   289  			case <-time.After(5 * time.Second):
   290  				t.Fatal("waited to long for request to happen")
   291  			}
   292  
   293  			assert.True(t, hookTask.IsSucceed)
   294  		})
   295  	}
   296  }