2019-11-09 00:25:53 +03:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2019-11-09 00:25:53 +03:00
package webhook
import (
2022-11-03 21:23:20 +03:00
"context"
2024-03-08 01:18:38 +03:00
"io"
2019-11-09 00:25:53 +03:00
"net/http"
2022-11-03 21:23:20 +03:00
"net/http/httptest"
2019-11-09 00:25:53 +03:00
"net/url"
2024-03-08 01:18:38 +03:00
"strings"
2019-11-09 00:25:53 +03:00
"testing"
2022-11-03 21:23:20 +03:00
"time"
2019-11-09 00:25:53 +03:00
2022-11-03 21:23:20 +03:00
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
webhook_model "code.gitea.io/gitea/models/webhook"
2023-10-18 12:44:36 +03:00
"code.gitea.io/gitea/modules/hostmatcher"
2019-11-09 00:25:53 +03:00
"code.gitea.io/gitea/modules/setting"
2024-03-14 04:10:51 +03:00
"code.gitea.io/gitea/modules/util"
2023-01-01 18:23:15 +03:00
webhook_module "code.gitea.io/gitea/modules/webhook"
2021-11-17 15:34:35 +03:00
2019-11-09 00:25:53 +03:00
"github.com/stretchr/testify/assert"
2023-10-18 12:44:36 +03:00
"github.com/stretchr/testify/require"
2019-11-09 00:25:53 +03:00
)
func TestWebhookProxy ( t * testing . T ) {
2023-10-18 12:44:36 +03:00
oldWebhook := setting . Webhook
t . Cleanup ( func ( ) {
setting . Webhook = oldWebhook
} )
2019-11-09 00:25:53 +03:00
setting . Webhook . ProxyURL = "http://localhost:8080"
setting . Webhook . ProxyURLFixed , _ = url . Parse ( setting . Webhook . ProxyURL )
setting . Webhook . ProxyHosts = [ ] string { "*.discordapp.com" , "discordapp.com" }
2023-10-18 12:44:36 +03:00
allowedHostMatcher := hostmatcher . ParseHostMatchList ( "webhook.ALLOWED_HOST_LIST" , "discordapp.com,s.discordapp.com" )
tests := [ ] struct {
req string
want string
wantErr bool
} {
{
req : "https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx" ,
want : "http://localhost:8080" ,
wantErr : false ,
} ,
{
req : "http://s.discordapp.com/assets/xxxxxx" ,
want : "http://localhost:8080" ,
wantErr : false ,
} ,
{
req : "http://github.com/a/b" ,
want : "" ,
wantErr : false ,
} ,
{
req : "http://www.discordapp.com/assets/xxxxxx" ,
want : "" ,
wantErr : true ,
} ,
2019-11-09 00:25:53 +03:00
}
2023-10-18 12:44:36 +03:00
for _ , tt := range tests {
t . Run ( tt . req , func ( t * testing . T ) {
req , err := http . NewRequest ( "POST" , tt . req , nil )
require . NoError ( t , err )
u , err := webhookProxy ( allowedHostMatcher ) ( req )
if tt . wantErr {
assert . Error ( t , err )
return
}
assert . NoError ( t , err )
2019-11-09 00:25:53 +03:00
2023-10-18 12:44:36 +03:00
got := ""
if u != nil {
got = u . String ( )
}
assert . Equal ( t , tt . want , got )
} )
2019-11-09 00:25:53 +03:00
}
}
2022-11-03 21:23:20 +03:00
func TestWebhookDeliverAuthorizationHeader ( t * testing . T ) {
assert . NoError ( t , unittest . PrepareTestDatabase ( ) )
done := make ( chan struct { } , 1 )
s := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
assert . Equal ( t , "/webhook" , r . URL . Path )
assert . Equal ( t , "Bearer s3cr3t-t0ken" , r . Header . Get ( "Authorization" ) )
w . WriteHeader ( 200 )
done <- struct { } { }
} ) )
t . Cleanup ( s . Close )
hook := & webhook_model . Webhook {
RepoID : 3 ,
URL : s . URL + "/webhook" ,
ContentType : webhook_model . ContentTypeJSON ,
IsActive : true ,
2023-01-01 18:23:15 +03:00
Type : webhook_module . GITEA ,
2022-11-03 21:23:20 +03:00
}
err := hook . SetHeaderAuthorization ( "Bearer s3cr3t-t0ken" )
assert . NoError ( t , err )
assert . NoError ( t , webhook_model . CreateWebhook ( db . DefaultContext , hook ) )
2024-03-08 01:18:38 +03:00
hookTask := & webhook_model . HookTask {
HookID : hook . ID ,
EventType : webhook_module . HookEventPush ,
PayloadVersion : 2 ,
}
2022-11-23 17:10:04 +03:00
hookTask , err = webhook_model . CreateHookTask ( db . DefaultContext , hookTask )
assert . NoError ( t , err )
2024-03-08 01:18:38 +03:00
assert . NotNil ( t , hookTask )
2022-11-03 21:23:20 +03:00
assert . NoError ( t , Deliver ( context . Background ( ) , hookTask ) )
select {
case <- done :
case <- time . After ( 5 * time . Second ) :
t . Fatal ( "waited to long for request to happen" )
}
assert . True ( t , hookTask . IsSucceed )
2024-03-08 01:18:38 +03:00
assert . Equal ( t , "******" , hookTask . RequestInfo . Headers [ "Authorization" ] )
}
func TestWebhookDeliverHookTask ( t * testing . T ) {
assert . NoError ( t , unittest . PrepareTestDatabase ( ) )
done := make ( chan struct { } , 1 )
s := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
assert . Equal ( t , "PUT" , r . Method )
switch r . URL . Path {
case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98" :
// Version 1
assert . Equal ( t , "push" , r . Header . Get ( "X-GitHub-Event" ) )
assert . Equal ( t , "" , r . Header . Get ( "Content-Type" ) )
body , err := io . ReadAll ( r . Body )
assert . NoError ( t , err )
assert . Equal ( t , ` { "data": 42} ` , string ( body ) )
case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51" :
// Version 2
assert . Equal ( t , "push" , r . Header . Get ( "X-GitHub-Event" ) )
assert . Equal ( t , "application/json" , r . Header . Get ( "Content-Type" ) )
body , err := io . ReadAll ( r . Body )
assert . NoError ( t , err )
assert . Len ( t , body , 2147 )
default :
w . WriteHeader ( 404 )
t . Fatalf ( "unexpected url path %s" , r . URL . Path )
return
}
w . WriteHeader ( 200 )
done <- struct { } { }
} ) )
t . Cleanup ( s . Close )
hook := & webhook_model . Webhook {
RepoID : 3 ,
IsActive : true ,
Type : webhook_module . MATRIX ,
URL : s . URL + "/webhook" ,
HTTPMethod : "PUT" ,
ContentType : webhook_model . ContentTypeJSON ,
Meta : ` { "message_type":0} ` , // text
}
assert . NoError ( t , webhook_model . CreateWebhook ( db . DefaultContext , hook ) )
t . Run ( "Version 1" , func ( t * testing . T ) {
hookTask := & webhook_model . HookTask {
HookID : hook . ID ,
EventType : webhook_module . HookEventPush ,
PayloadContent : ` { "data": 42} ` ,
PayloadVersion : 1 ,
}
hookTask , err := webhook_model . CreateHookTask ( db . DefaultContext , hookTask )
assert . NoError ( t , err )
assert . NotNil ( t , hookTask )
assert . NoError ( t , Deliver ( context . Background ( ) , hookTask ) )
select {
case <- done :
case <- time . After ( 5 * time . Second ) :
t . Fatal ( "waited to long for request to happen" )
}
assert . True ( t , hookTask . IsSucceed )
} )
t . Run ( "Version 2" , func ( t * testing . T ) {
p := pushTestPayload ( )
data , err := p . JSONPayload ( )
assert . NoError ( t , err )
hookTask := & webhook_model . HookTask {
HookID : hook . ID ,
EventType : webhook_module . HookEventPush ,
PayloadContent : string ( data ) ,
PayloadVersion : 2 ,
}
hookTask , err = webhook_model . CreateHookTask ( db . DefaultContext , hookTask )
assert . NoError ( t , err )
assert . NotNil ( t , hookTask )
assert . NoError ( t , Deliver ( context . Background ( ) , hookTask ) )
select {
case <- done :
case <- time . After ( 5 * time . Second ) :
t . Fatal ( "waited to long for request to happen" )
}
assert . True ( t , hookTask . IsSucceed )
} )
}
func TestWebhookDeliverSpecificTypes ( t * testing . T ) {
assert . NoError ( t , unittest . PrepareTestDatabase ( ) )
type hookCase struct {
2024-03-14 04:10:51 +03:00
gotBody chan [ ] byte
httpMethod string // default to POST
2024-03-08 01:18:38 +03:00
}
2024-03-14 04:10:51 +03:00
cases := map [ string ] * hookCase {
webhook_module . SLACK : { } ,
webhook_module . DISCORD : { } ,
webhook_module . DINGTALK : { } ,
webhook_module . TELEGRAM : { } ,
webhook_module . MSTEAMS : { } ,
webhook_module . FEISHU : { } ,
webhook_module . MATRIX : { httpMethod : "PUT" } ,
webhook_module . WECHATWORK : { } ,
webhook_module . PACKAGIST : { } ,
2024-03-08 01:18:38 +03:00
}
s := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2024-03-14 04:10:51 +03:00
typ := strings . Split ( r . URL . Path , "/" ) [ 1 ] // URL: "/{webhook_type}/other-path"
2024-03-08 01:18:38 +03:00
assert . Equal ( t , "application/json" , r . Header . Get ( "Content-Type" ) , r . URL . Path )
2024-03-14 04:10:51 +03:00
assert . Equal ( t , util . IfZero ( cases [ typ ] . httpMethod , "POST" ) , r . Method , "webhook test request %q" , r . URL . Path )
body , _ := io . ReadAll ( r . Body ) // read request and send it back to the test by testcase's chan
cases [ typ ] . gotBody <- body
w . WriteHeader ( http . StatusNoContent )
2024-03-08 01:18:38 +03:00
} ) )
t . Cleanup ( s . Close )
p := pushTestPayload ( )
data , err := p . JSONPayload ( )
assert . NoError ( t , err )
2024-03-14 04:10:51 +03:00
for typ := range cases {
cases [ typ ] . gotBody = make ( chan [ ] byte , 1 )
2024-03-08 01:18:38 +03:00
t . Run ( typ , func ( t * testing . T ) {
t . Parallel ( )
hook := & webhook_model . Webhook {
2024-03-14 04:10:51 +03:00
RepoID : 3 ,
IsActive : true ,
Type : typ ,
URL : s . URL + "/" + typ ,
Meta : "{}" ,
2024-03-08 01:18:38 +03:00
}
assert . NoError ( t , webhook_model . CreateWebhook ( db . DefaultContext , hook ) )
hookTask := & webhook_model . HookTask {
HookID : hook . ID ,
EventType : webhook_module . HookEventPush ,
PayloadContent : string ( data ) ,
PayloadVersion : 2 ,
}
hookTask , err := webhook_model . CreateHookTask ( db . DefaultContext , hookTask )
assert . NoError ( t , err )
assert . NotNil ( t , hookTask )
assert . NoError ( t , Deliver ( context . Background ( ) , hookTask ) )
2024-03-14 04:10:51 +03:00
2024-03-08 01:18:38 +03:00
select {
2024-03-14 04:10:51 +03:00
case gotBody := <- cases [ typ ] . gotBody :
2024-03-08 01:18:38 +03:00
assert . NotEqual ( t , string ( data ) , string ( gotBody ) , "request body must be different from the event payload" )
2024-03-14 04:10:51 +03:00
assert . Equal ( t , hookTask . RequestInfo . Body , string ( gotBody ) , "delivered webhook payload doesn't match saved request" )
2024-03-08 01:18:38 +03:00
case <- time . After ( 5 * time . Second ) :
t . Fatal ( "waited to long for request to happen" )
}
assert . True ( t , hookTask . IsSucceed )
} )
}
2022-11-03 21:23:20 +03:00
}