Skip to content

Commit

Permalink
Refactor GitHub mock server
Browse files Browse the repository at this point in the history
  • Loading branch information
int128 committed Jan 7, 2024
1 parent 6753940 commit bac6886
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 165 deletions.
14 changes: 14 additions & 0 deletions internal/controller/applicationhealthcomment_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package controller

import (
"context"
"net/http"
"time"

argocdv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/gitops-engine/pkg/health"
"github.com/int128/argocd-commenter/internal/controller/githubmock"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -42,6 +44,12 @@ var _ = Describe("Application health comment controller", func() {

Context("When an application is healthy", func() {
It("Should notify a comment once", func(ctx context.Context) {
githubMock.AddHandlers(map[string]http.HandlerFunc{
"GET /api/v3/repos/int128/manifests/commits/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa200/pulls": githubmock.ListPullRequestsWithCommit(200),
"GET /api/v3/repos/int128/manifests/pulls/200/files": githubmock.ListFiles(),
"POST /api/v3/repos/int128/manifests/issues/200/comments": githubMock.CreateComment(200),
})

By("Updating the application to progressing")
app.Status = argocdv1alpha1.ApplicationStatus{
Health: argocdv1alpha1.HealthStatus{
Expand Down Expand Up @@ -76,6 +84,12 @@ var _ = Describe("Application health comment controller", func() {

Context("When an application is degraded and then healthy", func() {
It("Should notify a comment for degraded and healthy", func(ctx context.Context) {
githubMock.AddHandlers(map[string]http.HandlerFunc{
"GET /api/v3/repos/int128/manifests/commits/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa201/pulls": githubmock.ListPullRequestsWithCommit(201),
"GET /api/v3/repos/int128/manifests/pulls/201/files": githubmock.ListFiles(),
"POST /api/v3/repos/int128/manifests/issues/201/comments": githubMock.CreateComment(201),
})

By("Updating the application to progressing")
app.Status = argocdv1alpha1.ApplicationStatus{
Health: argocdv1alpha1.HealthStatus{
Expand Down
27 changes: 24 additions & 3 deletions internal/controller/applicationhealthdeployment_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package controller

import (
"context"
"net/http"
"time"

argocdv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
Expand Down Expand Up @@ -60,6 +61,10 @@ var _ = Describe("Application health deployment controller", func() {

Context("When an application is healthy", func() {
It("Should notify a deployment status once", func(ctx context.Context) {
githubMock.AddHandlers(map[string]http.HandlerFunc{
"GET /api/v3/repos/int128/manifests/deployments/999300/statuses": githubMock.ListDeploymentStatus(999300),
"POST /api/v3/repos/int128/manifests/deployments/999300/statuses": githubMock.CreateDeploymentStatus(999300),
})
githubMock.DeploymentStatuses.SetResponse(999300, []*github.DeploymentStatus{})

By("Updating the deployment annotation")
Expand Down Expand Up @@ -91,11 +96,16 @@ var _ = Describe("Application health deployment controller", func() {

Context("When the deployment annotation is updated and then the application becomes healthy", func() {
It("Should notify a deployment status", func(ctx context.Context) {
githubMock.AddHandlers(map[string]http.HandlerFunc{
"GET /api/v3/repos/int128/manifests/deployments/999301/statuses": githubMock.ListDeploymentStatus(999301),
"POST /api/v3/repos/int128/manifests/deployments/999301/statuses": githubMock.CreateDeploymentStatus(999301),
"GET /api/v3/repos/int128/manifests/deployments/999991/statuses": http.NotFound,
})
githubMock.DeploymentStatuses.SetResponse(999301, []*github.DeploymentStatus{})

By("Updating the deployment annotation")
app.Annotations = map[string]string{
"argocd-commenter.int128.github.io/deployment-url": "https://api.github.com/repos/int128/manifests/deployments/999999",
"argocd-commenter.int128.github.io/deployment-url": "https://api.github.com/repos/int128/manifests/deployments/999991",
}
Expect(k8sClient.Update(ctx, &app)).Should(Succeed())

Expand Down Expand Up @@ -123,11 +133,18 @@ var _ = Describe("Application health deployment controller", func() {

Context("When an application became healthy before the deployment annotation is updated", func() {
It("Should notify a deployment status when the deployment annotation is valid", func(ctx context.Context) {
githubMock.AddHandlers(map[string]http.HandlerFunc{
"GET /api/v3/repos/int128/manifests/deployments/999302/statuses": githubMock.ListDeploymentStatus(999302),
"POST /api/v3/repos/int128/manifests/deployments/999302/statuses": githubMock.CreateDeploymentStatus(999302),
"GET /api/v3/repos/int128/manifests/deployments/999303/statuses": githubMock.ListDeploymentStatus(999303),
"POST /api/v3/repos/int128/manifests/deployments/999303/statuses": githubMock.CreateDeploymentStatus(999303),
"GET /api/v3/repos/int128/manifests/deployments/999992/statuses": http.NotFound,
})
githubMock.DeploymentStatuses.SetResponse(999302, []*github.DeploymentStatus{})

By("Updating the deployment annotation")
app.Annotations = map[string]string{
"argocd-commenter.int128.github.io/deployment-url": "https://api.github.com/repos/int128/manifests/deployments/999999",
"argocd-commenter.int128.github.io/deployment-url": "https://api.github.com/repos/int128/manifests/deployments/999992",
}
Expect(k8sClient.Update(ctx, &app)).Should(Succeed())

Expand Down Expand Up @@ -167,9 +184,13 @@ var _ = Describe("Application health deployment controller", func() {
}, SpecTimeout(3*time.Second))

It("Should retry a deployment status until timeout", func(ctx context.Context) {
githubMock.AddHandlers(map[string]http.HandlerFunc{
"GET /api/v3/repos/int128/manifests/deployments/999994/statuses": http.NotFound,
})

By("Updating the deployment annotation")
app.Annotations = map[string]string{
"argocd-commenter.int128.github.io/deployment-url": "https://api.github.com/repos/int128/manifests/deployments/999999",
"argocd-commenter.int128.github.io/deployment-url": "https://api.github.com/repos/int128/manifests/deployments/999994",
}
app.Status = argocdv1alpha1.ApplicationStatus{
OperationState: &argocdv1alpha1.OperationState{
Expand Down
14 changes: 14 additions & 0 deletions internal/controller/applicationphasecomment_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package controller

import (
"context"
"net/http"
"time"

argocdv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
"github.com/int128/argocd-commenter/internal/controller/githubmock"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -42,6 +44,12 @@ var _ = Describe("Application phase controller", func() {

Context("When an application is synced", func() {
It("Should notify a comment", func(ctx context.Context) {
githubMock.AddHandlers(map[string]http.HandlerFunc{
"GET /api/v3/repos/int128/manifests/commits/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa100/pulls": githubmock.ListPullRequestsWithCommit(100),
"GET /api/v3/repos/int128/manifests/pulls/100/files": githubmock.ListFiles(),
"POST /api/v3/repos/int128/manifests/issues/100/comments": githubMock.CreateComment(100),
})

By("Updating the application to running")
app.Status = argocdv1alpha1.ApplicationStatus{
OperationState: &argocdv1alpha1.OperationState{
Expand All @@ -66,6 +74,12 @@ var _ = Describe("Application phase controller", func() {

Context("When an application sync operation is failed", func() {
It("Should notify a comment", func(ctx context.Context) {
githubMock.AddHandlers(map[string]http.HandlerFunc{
"GET /api/v3/repos/int128/manifests/commits/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa101/pulls": githubmock.ListPullRequestsWithCommit(101),
"GET /api/v3/repos/int128/manifests/pulls/101/files": githubmock.ListFiles(),
"POST /api/v3/repos/int128/manifests/issues/101/comments": githubMock.CreateComment(101),
})

By("Updating the application to running")
app.Status = argocdv1alpha1.ApplicationStatus{
OperationState: &argocdv1alpha1.OperationState{
Expand Down
13 changes: 13 additions & 0 deletions internal/controller/applicationphasedeployment_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package controller

import (
"context"
"net/http"
"time"

argocdv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
Expand Down Expand Up @@ -44,6 +45,10 @@ var _ = Describe("Application phase controller", func() {

Context("When an application is synced", func() {
It("Should notify a deployment status", func(ctx context.Context) {
githubMock.AddHandlers(map[string]http.HandlerFunc{
"GET /api/v3/repos/int128/manifests/deployments/999100/statuses": githubMock.ListDeploymentStatus(999100),
"POST /api/v3/repos/int128/manifests/deployments/999100/statuses": githubMock.CreateDeploymentStatus(999100),
})
githubMock.DeploymentStatuses.SetResponse(999100, []*github.DeploymentStatus{})

By("Updating the deployment annotation")
Expand Down Expand Up @@ -90,6 +95,10 @@ var _ = Describe("Application phase controller", func() {

Context("When an application sync operation is failed", func() {
It("Should notify a deployment status", func(ctx context.Context) {
githubMock.AddHandlers(map[string]http.HandlerFunc{
"GET /api/v3/repos/int128/manifests/deployments/999101/statuses": githubMock.ListDeploymentStatus(999101),
"POST /api/v3/repos/int128/manifests/deployments/999101/statuses": githubMock.CreateDeploymentStatus(999101),
})
githubMock.DeploymentStatuses.SetResponse(999101, []*github.DeploymentStatus{})

By("Updating the deployment annotation")
Expand Down Expand Up @@ -122,6 +131,10 @@ var _ = Describe("Application phase controller", func() {

Context("When an application was synced before the deployment annotation is updated", func() {
It("Should skip the notification", func(ctx context.Context) {
githubMock.AddHandlers(map[string]http.HandlerFunc{
"GET /api/v3/repos/int128/manifests/deployments/999102/statuses": githubMock.ListDeploymentStatus(999102),
"POST /api/v3/repos/int128/manifests/deployments/999102/statuses": githubMock.CreateDeploymentStatus(999102),
})
githubMock.DeploymentStatuses.SetResponse(999102, []*github.DeploymentStatus{})

By("Updating the deployment annotation")
Expand Down
139 changes: 139 additions & 0 deletions internal/controller/githubmock/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package githubmock

import (
"encoding/json"
"fmt"
"io"
"maps"
"net/http"
"strings"
"sync"

"github.com/google/go-github/v57/github"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

type Server struct {
mu sync.Mutex
handlers map[string]http.HandlerFunc

Comments Endpoint[int, any]
DeploymentStatuses Endpoint[int, []*github.DeploymentStatus]
}

func (sv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer GinkgoRecover()
methodURI := fmt.Sprintf("%s %s", r.Method, r.RequestURI)
GinkgoWriter.Println("GITHUB", methodURI)
handler := sv.getHandler(methodURI)
if handler == nil {
http.NotFound(w, r)
return
}
handler(w, r)
}

func (sv *Server) getHandler(methodURI string) http.HandlerFunc {
sv.mu.Lock()
defer sv.mu.Unlock()
return sv.handlers[methodURI]
}

func (sv *Server) AddHandlers(handlers map[string]http.HandlerFunc) {
sv.mu.Lock()
defer sv.mu.Unlock()
if sv.handlers == nil {
sv.handlers = make(map[string]http.HandlerFunc)
}
maps.Copy(sv.handlers, handlers)
}

func ListPullRequestsWithCommit(number int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("content-type", "application/json")
w.WriteHeader(200)
Expect(json.NewEncoder(w).Encode([]*github.PullRequest{{Number: github.Int(number)}})).Should(Succeed())
}
}

func ListFiles() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("content-type", "application/json")
w.WriteHeader(200)
Expect(json.NewEncoder(w).Encode([]*github.CommitFile{{Filename: github.String("test/deployment.yaml")}})).Should(Succeed())
}
}

func (sv *Server) CreateComment(number int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("content-type", "application/json")
w.WriteHeader(200)
sv.Comments.call(number)
b, err := io.ReadAll(r.Body)
Expect(err).Should(Succeed())
GinkgoWriter.Println("GITHUB", "created comment", strings.TrimSpace(string(b)))
}
}

func (sv *Server) CreateDeploymentStatus(id int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("content-type", "application/json")
w.WriteHeader(200)
sv.DeploymentStatuses.call(id)
var ds github.DeploymentStatusRequest
Expect(json.NewDecoder(r.Body).Decode(&ds)).Should(Succeed())
GinkgoWriter.Println("GITHUB", "created deployment status", ds)
sv.DeploymentStatuses.SetResponse(id, []*github.DeploymentStatus{{State: ds.State}})
}
}

func (sv *Server) ListDeploymentStatus(id int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ds := sv.DeploymentStatuses.getResponse(id)
if ds == nil {
http.NotFound(w, r)
return
}
w.Header().Add("content-type", "application/json")
w.WriteHeader(200)
Expect(json.NewEncoder(w).Encode(ds)).Should(Succeed())
}
}

type Endpoint[K int | string, V interface{}] struct {
mu sync.Mutex
counter map[K]int
response map[K]V
}

func (r *Endpoint[K, V]) CountBy(key K) int {
r.mu.Lock()
defer r.mu.Unlock()
return r.counter[key]
}

func (r *Endpoint[K, V]) call(key K) int {
r.mu.Lock()
defer r.mu.Unlock()
if r.counter == nil {
r.counter = make(map[K]int)
}
r.counter[key]++
return r.counter[key]
}

func (r *Endpoint[K, V]) getResponse(k K) V {
r.mu.Lock()
defer r.mu.Unlock()
return r.response[k]
}

func (r *Endpoint[K, V]) SetResponse(k K, v V) {
r.mu.Lock()
defer r.mu.Unlock()
if r.response == nil {
r.response = make(map[K]V)
}
r.response[k] = v
}
Loading

0 comments on commit bac6886

Please sign in to comment.