Skip to content

Commit

Permalink
Try to migrate most of the cards into one unified one which is consis…
Browse files Browse the repository at this point in the history
…tently shown
  • Loading branch information
J12934 committed Nov 10, 2024
1 parent cca6161 commit 5c10080
Show file tree
Hide file tree
Showing 16 changed files with 523 additions and 338 deletions.
1 change: 1 addition & 0 deletions balancer/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func AddRoutes(
router.Handle("POST /balancer/api/teams/logout", handleLogout(bundle))
router.Handle("POST /balancer/api/teams/reset-passcode", handleResetPasscode(bundle))
router.Handle("GET /balancer/api/score-board/top", handleScoreBoard(bundle))
router.Handle("GET /balancer/api/teams/status", handleTeamStatus(bundle))

router.Handle("GET /balancer/api/admin/all", handleAdminListInstances(bundle))
router.Handle("DELETE /balancer/api/admin/teams/{team}/delete", handleAdminDeleteInstance(bundle))
Expand Down
78 changes: 78 additions & 0 deletions balancer/routes/teamStatus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package routes

import (
"encoding/json"
"fmt"
"net/http"

"github.com/juice-shop/multi-juicer/balancer/pkg/bundle"
"github.com/juice-shop/multi-juicer/balancer/pkg/scoring"
"github.com/juice-shop/multi-juicer/balancer/pkg/teamcookie"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type TeamStatus struct {
Name string `json:"name"`
Score int `json:"score"`
SolvedChallenges int `json:"solvedChallenges"`
Position int `json:"position"`
TotalTeams int `json:"totalTeams"`
Readiness bool `json:"readiness"`
}

func handleTeamStatus(bundle *bundle.Bundle) http.Handler {
return http.HandlerFunc(
func(responseWriter http.ResponseWriter, req *http.Request) {
team, err := teamcookie.GetTeamFromRequest(bundle, req)
if err != nil {
http.Error(responseWriter, "", http.StatusUnauthorized)
return
}

bundle.Log.Printf("Awaiting readiness of JuiceShop Deployment for team '%s'", team)

deployment, err := bundle.ClientSet.AppsV1().Deployments(bundle.RuntimeEnvironment.Namespace).Get(req.Context(), fmt.Sprintf("juiceshop-%s", team), metav1.GetOptions{})
if err != nil {
http.Error(responseWriter, "team not found", http.StatusNotFound)
return
}

currentScores := scoring.GetScores()
var teamScore scoring.TeamScore
for _, score := range currentScores {
if score.Name == team {
teamScore = score
break
}
}
teamCount := len(currentScores)
if teamScore.Name == "" {
// the team is not in the score board, it should be there the next time the score-board worker finishes
teamScore.Score = -1
teamScore.Position = -1
// increment the total count by one as we know that this teams hasn't been counted yet
teamCount++
}

response := TeamStatus{
Name: team,
Score: teamScore.Score,
Position: teamScore.Position,
TotalTeams: teamCount,
SolvedChallenges: len(teamScore.Challenges),
Readiness: deployment.Status.ReadyReplicas == 1,
}

responseBytes, err := json.Marshal(response)
if err != nil {
bundle.Log.Printf("Failed to marshal response: %s", err)
http.Error(responseWriter, "", http.StatusInternalServerError)
return
}

responseWriter.Header().Set("Content-Type", "application/json")
responseWriter.Write(responseBytes)
responseWriter.WriteHeader(http.StatusOK)
},
)
}
119 changes: 119 additions & 0 deletions balancer/routes/teamStatus_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package routes

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

b "github.com/juice-shop/multi-juicer/balancer/pkg/bundle"
"github.com/juice-shop/multi-juicer/balancer/pkg/scoring"
"github.com/juice-shop/multi-juicer/balancer/pkg/testutil"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)

func TestTeamStatusHandler(t *testing.T) {
team := "foobar"

createTeam := func(team string, challenges string, solvedChallenges string) *appsv1.Deployment {
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("juiceshop-%s", team),
Namespace: "test-namespace",
Annotations: map[string]string{
"multi-juicer.owasp-juice.shop/challenges": challenges,
"multi-juicer.owasp-juice.shop/challengesSolved": solvedChallenges,
},
Labels: map[string]string{
"app.kubernetes.io/name": "juice-shop",
"app.kubernetes.io/part-of": "multi-juicer",
"team": team,
},
},
Status: appsv1.DeploymentStatus{
ReadyReplicas: 1,
},
}
}

t.Run("returns the instance status", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/balancer/api/teams/status", nil)
req.Header.Set("Cookie", fmt.Sprintf("team=%s", testutil.SignTestTeamname(team)))
rr := httptest.NewRecorder()
server := http.NewServeMux()
clientset := fake.NewSimpleClientset(
createTeam("foobar", `[{"key":"scoreBoardChallenge","solvedAt":"2024-11-01T19:55:48.211Z"}]`, "1"),
createTeam("barfoo", `[]`, "0"),
)
bundle := testutil.NewTestBundleWithCustomFakeClient(clientset)
scoring.CalculateAndCacheScoreBoard(context.Background(), bundle, map[string]b.JuiceShopChallenge{
"scoreBoardChallenge": {
Key: "scoreBoardChallenge",
Difficulty: 1,
},
})
AddRoutes(server, bundle)

server.ServeHTTP(rr, req)

assert.Equal(t, http.StatusOK, rr.Code)
assert.JSONEq(t, `{"name":"foobar","score":10,"position":1,"solvedChallenges":1,"totalTeams":2,"readiness":true}`, rr.Body.String())
})

t.Run("returns -1 for position and score if it hasn't been calculated yet", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/balancer/api/teams/status", nil)
req.Header.Set("Cookie", fmt.Sprintf("team=%s", testutil.SignTestTeamname(team)))
rr := httptest.NewRecorder()
server := http.NewServeMux()
clientset := fake.NewSimpleClientset(createTeam("other-team", `[{"key":"scoreBoardChallenge","solvedAt":"2024-11-01T19:55:48.211Z"}]`, "1"))
bundle := testutil.NewTestBundleWithCustomFakeClient(clientset)
AddRoutes(server, bundle)
scoring.CalculateAndCacheScoreBoard(context.Background(), bundle, map[string]b.JuiceShopChallenge{
"scoreBoardChallenge": {
Key: "scoreBoardChallenge",
Difficulty: 1,
},
})
clientset.AppsV1().Deployments(bundle.RuntimeEnvironment.Namespace).Create(context.Background(), createTeam("foobar", `[{"key":"scoreBoardChallenge","solvedAt":"2024-11-01T19:55:48.211Z"}]`, "1"), metav1.CreateOptions{})

server.ServeHTTP(rr, req)

assert.Equal(t, http.StatusOK, rr.Code)
assert.JSONEq(t, `{"name":"foobar","score":-1,"position":-1,"solvedChallenges":0,"totalTeams":2,"readiness":true}`, rr.Body.String())
})

t.Run("returns a 404 if the team doesn't have a deployment", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/balancer/api/teams/status", nil)
req.Header.Set("Cookie", fmt.Sprintf("team=%s", testutil.SignTestTeamname(team)))
rr := httptest.NewRecorder()
server := http.NewServeMux()

bundle := testutil.NewTestBundle()
AddRoutes(server, bundle)

server.ServeHTTP(rr, req)

assert.Equal(t, http.StatusNotFound, rr.Code)
assert.Contains(t, rr.Body.String(), "team not found")
})

t.Run("returns a 401 if the balancer cookie isn't signed", func(t *testing.T) {
req, _ := http.NewRequest("GET", fmt.Sprintf("/balancer/api/teams/%s/wait-till-ready", team), nil)
req.Header.Set("Cookie", fmt.Sprintf("team=%s", team))
rr := httptest.NewRecorder()
server := http.NewServeMux()

bundle := testutil.NewTestBundle()
AddRoutes(server, bundle)

server.ServeHTTP(rr, req)

assert.Equal(t, http.StatusUnauthorized, rr.Code)
assert.Equal(t, "", strings.TrimSpace(rr.Body.String()))
})
}
33 changes: 33 additions & 0 deletions balancer/ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions balancer/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"devDependencies": {
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
"@types/promise-retry": "^1.1.6",
"@vitejs/plugin-react": "^4.3.2",
"jsdom": "^25.0.1",
"vite": "^5.4.8",
Expand Down
67 changes: 67 additions & 0 deletions balancer/ui/public/icons/astronaut.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions balancer/ui/public/icons/warning.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions balancer/ui/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { IntlProvider } from "react-intl";

import { JoinPage } from "./pages/JoinPage";
import { JoiningPage } from "./pages/JoiningPage";
import { JoinedPage } from "./pages/JoinedPage";
import { ScoreBoard } from "./pages/ScoreBoard.tsx";
import { TeamStatusPage } from "./pages/TeamStatusPage.tsx";

import { Layout } from "./Layout.tsx";
import { Spinner } from "./Spinner.tsx";
Expand Down Expand Up @@ -43,8 +43,8 @@ function App() {
<Routes>
<Route path="/" exact element={<JoinPage />} />
<Route path="/admin" element={<AdminPage />} />
<Route path="/teams/:team/status/" element={<TeamStatusPage />} />
<Route path="/teams/:team/joining/" element={<JoiningPage />} />
<Route path="/teams/:team/joined/" element={<JoinedPage />} />
<Route path="/score-board/" element={<ScoreBoard />} />
</Routes>
</Suspense>
Expand Down
Loading

0 comments on commit 5c10080

Please sign in to comment.