gitforge/services/webhook/msteams_test.go
nightfurysl2001 b58d8e20fd feat: Update Microsoft Teams webhook to use AdaptiveCard (#11704)
## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests for Go changes

(can be removed for JavaScript changes)

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I ran...
  - [ ] `make pr-go` before pushing

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
- [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.

Closes #4517

<!--start release-notes-assistant-->

## Release notes
<!--URL:https://codeberg.org/forgejo/forgejo-->
- Features
  - [PR](https://codeberg.org/forgejo/forgejo/pulls/11704): <!--number 11704 --><!--line 0 --><!--description VXBkYXRlIE1pY3Jvc29mdCBUZWFtcyB3ZWJob29rIHRvIHVzZSBBZGFwdGl2ZUNhcmQ=-->Update Microsoft Teams webhook to use AdaptiveCard<!--description-->
<!--end release-notes-assistant-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11704
Reviewed-by: oliverpool <oliverpool@noreply.codeberg.org>
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
2026-05-26 16:10:55 +02:00

416 lines
12 KiB
Go

// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package webhook
import (
"bytes"
"context"
"strings"
"testing"
webhook_model "forgejo.org/models/webhook"
"forgejo.org/modules/json"
api "forgejo.org/modules/structs"
webhook_module "forgejo.org/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// findTextInContainer recursively searches for text within an MSTeamsContainer
func findTextInContainer(c MSTeamsContainer, substr string) bool {
for _, it := range c.Items {
switch v := it.(type) {
case MSTeamsTextBlock:
if strings.Contains(v.Text, substr) {
return true
}
case MSTeamsColumnSet:
for _, col := range v.Columns {
for _, it2 := range col.Items {
if tb, ok := it2.(MSTeamsTextBlock); ok && strings.Contains(tb.Text, substr) {
return true
}
}
}
case MSTeamsContainer:
if findTextInContainer(v, substr) {
return true
}
case MSTeamsFactSet:
for _, fact := range v.Facts {
if strings.Contains(fact.Value, substr) || strings.Contains(fact.Title, substr) {
return true
}
}
}
}
return false
}
func TestMSTeamsPayload(t *testing.T) {
mc := msteamsConvertor{}
// helper to find text within the adaptive card body
findTextInBody := func(pl MSTeamsPayload, substr string) bool {
for _, container := range pl.Body {
if findTextInContainer(container, substr) {
return true
}
}
return false
}
t.Run("Create", func(t *testing.T) {
p := createTestPayload()
pl, err := mc.Create(p)
require.NoError(t, err)
require.NotNil(t, pl)
// Check payload structure
require.Equal(t, "AdaptiveCard", pl.Type)
require.Equal(t, "1.5", pl.Version)
// Check body structure: header + title + badge sections
require.GreaterOrEqual(t, len(pl.Body), 2)
// Header should contain repo info
assert.True(t, findTextInBody(pl, "test/repo"))
// Title should contain action by user
assert.True(t, findTextInBody(pl, "Branch created: test"))
// action button should point to branch
require.Len(t, pl.Actions, 1)
assert.Equal(t, "View in Forgejo", pl.Actions[0].Title)
assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Actions[0].URL)
})
t.Run("Delete", func(t *testing.T) {
p := deleteTestPayload()
pl, err := mc.Delete(p)
require.NoError(t, err)
require.NotNil(t, pl)
// Check basic structure
require.Equal(t, "AdaptiveCard", pl.Type)
require.GreaterOrEqual(t, len(pl.Body), 2)
// Verify content
assert.True(t, findTextInBody(pl, "test/repo"))
assert.True(t, findTextInBody(pl, "Branch deleted: test"))
// action button should point to branch
require.Len(t, pl.Actions, 1)
assert.Equal(t, "View in Forgejo", pl.Actions[0].Title)
assert.Equal(t, "http://localhost:3000/test/repo", pl.Actions[0].URL)
})
t.Run("Fork", func(t *testing.T) {
p := forkTestPayload()
pl, err := mc.Fork(p)
require.NoError(t, err)
require.NotNil(t, pl)
// Check basic structure
require.Equal(t, "AdaptiveCard", pl.Type)
require.GreaterOrEqual(t, len(pl.Body), 2)
// Verify content
assert.True(t, findTextInBody(pl, "[test/repo2](http://localhost:3000/test/repo2) is forked to test/repo"))
// action button should point to repo
require.Len(t, pl.Actions, 1)
assert.Equal(t, "View in Forgejo", pl.Actions[0].Title)
assert.Equal(t, "http://localhost:3000/test/repo", pl.Actions[0].URL)
})
t.Run("Push", func(t *testing.T) {
p := pushTestPayload()
pl, err := mc.Push(p)
require.NoError(t, err)
require.NotNil(t, pl)
// Check basic structure
require.Equal(t, "AdaptiveCard", pl.Type)
require.GreaterOrEqual(t, len(pl.Body), 2)
// Verify repo and basic content
assert.True(t, findTextInBody(pl, "[test] 2 new commits"))
// commit details should be present in body
assert.True(t, findTextInBody(pl, "2020558"))
assert.True(t, findTextInBody(pl, "commit message"))
// action button should point to compare
require.Len(t, pl.Actions, 1)
assert.Equal(t, "View in Forgejo", pl.Actions[0].Title)
assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Actions[0].URL)
})
t.Run("Issue", func(t *testing.T) {
p := issueTestPayload()
p.Action = api.HookIssueOpened
pl, err := mc.Issue(p)
require.NoError(t, err)
require.NotNil(t, pl)
// Check basic structure
require.Equal(t, "AdaptiveCard", pl.Type)
require.GreaterOrEqual(t, len(pl.Body), 2)
// Verify content
assert.True(t, findTextInBody(pl, "test/repo"))
assert.True(t, findTextInBody(pl, "Issue opened: #2 crash"))
assert.True(t, findTextInBody(pl, "issue body"))
require.Len(t, pl.Actions, 1)
assert.Equal(t, "View in Forgejo", pl.Actions[0].Title)
assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.Actions[0].URL)
p.Action = api.HookIssueClosed
pl, err = mc.Issue(p)
require.NoError(t, err)
require.NotNil(t, pl)
assert.True(t, findTextInBody(pl, "Issue closed: #2 crash"))
require.Len(t, pl.Actions, 1)
assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.Actions[0].URL)
})
t.Run("IssueComment", func(t *testing.T) {
p := issueCommentTestPayload()
pl, err := mc.IssueComment(p)
require.NoError(t, err)
require.NotNil(t, pl)
// Check basic structure
require.Equal(t, "AdaptiveCard", pl.Type)
require.GreaterOrEqual(t, len(pl.Body), 2)
// Verify content
assert.True(t, findTextInBody(pl, "test/repo"))
assert.True(t, findTextInBody(pl, "New comment on issue #2 crash"))
assert.True(t, findTextInBody(pl, "more info needed"))
require.Len(t, pl.Actions, 1)
assert.Equal(t, "View in Forgejo", pl.Actions[0].Title)
assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.Actions[0].URL)
})
t.Run("PullRequest", func(t *testing.T) {
p := pullRequestTestPayload()
p.PullRequest.Head = &api.PRBranchInfo{
Name: "feature/test",
Ref: "feature/test",
Sha: "b1eb92dc659513b7b4eb57d7ee7f9c6f92e714b5",
RepoID: 1,
Repository: &api.Repository{
HTMLURL: "http://localhost:3000/test/repo",
Name: "repo",
FullName: "test/repo",
},
}
pl, err := mc.PullRequest(p)
require.NoError(t, err)
require.NotNil(t, pl)
// Check basic structure
require.Equal(t, "AdaptiveCard", pl.Type)
require.GreaterOrEqual(t, len(pl.Body), 2)
// Verify content
assert.True(t, findTextInBody(pl, "test/repo"))
assert.True(t, findTextInBody(pl, "Pull request opened: #12 Fix bug"))
assert.True(t, findTextInBody(pl, "fixes bug #2"))
assert.True(t, findTextInBody(pl, "feature/test → refs/pull/2/head"))
require.Len(t, pl.Actions, 1)
assert.Equal(t, "View in Forgejo", pl.Actions[0].Title)
assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.Actions[0].URL)
})
t.Run("PullRequestComment", func(t *testing.T) {
p := pullRequestCommentTestPayload()
pl, err := mc.IssueComment(p)
require.NoError(t, err)
require.NotNil(t, pl)
// Check basic structure
require.Equal(t, "AdaptiveCard", pl.Type)
require.GreaterOrEqual(t, len(pl.Body), 2)
// Verify content
assert.True(t, findTextInBody(pl, "test/repo"))
assert.True(t, findTextInBody(pl, "New comment on pull request #12 Fix bug"))
assert.True(t, findTextInBody(pl, "changes requested"))
require.Len(t, pl.Actions, 1)
assert.Equal(t, "View in Forgejo", pl.Actions[0].Title)
assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.Actions[0].URL)
})
t.Run("Review", func(t *testing.T) {
p := pullRequestTestPayload()
p.Action = api.HookIssueReviewed
pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
require.NotNil(t, pl)
// Check basic structure
require.Equal(t, "AdaptiveCard", pl.Type)
require.GreaterOrEqual(t, len(pl.Body), 2)
// review content should be present
assert.True(t, findTextInBody(pl, "Pull request review approved: #12 Fix bug"))
assert.True(t, findTextInBody(pl, "good job"))
require.Len(t, pl.Actions, 1)
assert.Equal(t, "View in Forgejo", pl.Actions[0].Title)
assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.Actions[0].URL)
})
t.Run("Repository", func(t *testing.T) {
p := repositoryTestPayload()
pl, err := mc.Repository(p)
require.NoError(t, err)
require.NotNil(t, pl)
// Check basic structure
require.Equal(t, "AdaptiveCard", pl.Type)
require.GreaterOrEqual(t, len(pl.Body), 2)
// Verify content
assert.True(t, findTextInBody(pl, "Repository created: test/repo"))
require.Len(t, pl.Actions, 1)
assert.Equal(t, "View in Forgejo", pl.Actions[0].Title)
assert.Equal(t, "http://localhost:3000/test/repo", pl.Actions[0].URL)
})
t.Run("Package", func(t *testing.T) {
p := packageTestPayload()
pl, err := mc.Package(p)
require.NoError(t, err)
require.NotNil(t, pl)
// Check basic structure
require.Equal(t, "AdaptiveCard", pl.Type)
require.GreaterOrEqual(t, len(pl.Body), 2)
// no repo is associated
assert.False(t, findTextInBody(pl, "test/repo"))
// Verify content
assert.True(t, findTextInBody(pl, "Package created: GiteaContainer:latest"))
require.Len(t, pl.Actions, 1)
assert.Equal(t, "View in Forgejo", pl.Actions[0].Title)
assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.Actions[0].URL)
})
t.Run("Wiki", func(t *testing.T) {
p := wikiTestPayload()
p.Action = api.HookWikiCreated
pl, err := mc.Wiki(p)
require.NoError(t, err)
require.NotNil(t, pl)
// Check basic structure
require.Equal(t, "AdaptiveCard", pl.Type)
require.GreaterOrEqual(t, len(pl.Body), 2)
// Verify content for create
assert.True(t, findTextInBody(pl, "New wiki page \"index\""))
assert.True(t, findTextInBody(pl, "Wiki change comment"))
require.Len(t, pl.Actions, 1)
assert.Equal(t, "View in Forgejo", pl.Actions[0].Title)
assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Actions[0].URL)
p.Action = api.HookWikiEdited
pl, err = mc.Wiki(p)
require.NoError(t, err)
require.NotNil(t, pl)
assert.True(t, findTextInBody(pl, "Wiki page \"index\" edited"))
assert.True(t, findTextInBody(pl, "Wiki change comment"))
require.Len(t, pl.Actions, 1)
assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Actions[0].URL)
p.Action = api.HookWikiDeleted
pl, err = mc.Wiki(p)
require.NoError(t, err)
require.NotNil(t, pl)
assert.True(t, findTextInBody(pl, "Wiki page \"index\" deleted"))
require.Len(t, pl.Actions, 1)
assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Actions[0].URL)
})
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
pl, err := mc.Release(p)
require.NoError(t, err)
require.NotNil(t, pl)
// Check basic structure
require.Equal(t, "AdaptiveCard", pl.Type)
require.GreaterOrEqual(t, len(pl.Body), 2)
// Verify content
assert.True(t, findTextInBody(pl, "Release created: v1.0"))
require.Len(t, pl.Actions, 1)
assert.Equal(t, "View in Forgejo", pl.Actions[0].Title)
assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.Actions[0].URL)
})
}
func TestMSTeamsJSONPayload(t *testing.T) {
p := pushTestPayload()
data, err := p.JSONPayload()
require.NoError(t, err)
require.NotNil(t, data)
hook := &webhook_model.Webhook{
RepoID: 3,
IsActive: true,
Type: webhook_module.MSTEAMS,
URL: "https://msteams.example.com/",
Meta: ``,
HTTPMethod: "POST",
}
task := &webhook_model.HookTask{
HookID: hook.ID,
EventType: webhook_module.HookEventPush,
PayloadContent: string(data),
PayloadVersion: 2,
}
req, reqBody, err := msteamsHandler{}.NewRequest(context.Background(), hook, task)
require.NotNil(t, req)
require.NotNil(t, reqBody)
require.NoError(t, err)
assert.Equal(t, "POST", req.Method)
assert.Equal(t, "https://msteams.example.com/", req.URL.String())
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
var body MSTeamsPayload
err = json.NewDecoder(bytes.NewReader(reqBody)).Decode(&body)
require.NoError(t, err)
// Verify payload structure
assert.Equal(t, "AdaptiveCard", body.Type)
assert.Equal(t, "1.5", body.Version)
}