75f3dd141c
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CI and Release / lint-frontend (push) Has been cancelled
CI and Release / dockerfile-scan (push) Has been cancelled
CI and Release / test-frontend (push) Has been cancelled
CI and Release / lint-verification-agent (push) Has been cancelled
CI and Release / test-verification-agent (push) Has been cancelled
CI and Release / e2e-verification-agent (push) Has been cancelled
CI and Release / test-backend (push) Has been cancelled
CI and Release / build-dev-image (push) Has been cancelled
CI and Release / push-dev-image (push) Has been cancelled
CI and Release / build-image (push) Has been cancelled
CI and Release / build-verification-image (push) Has been cancelled
CI and Release / determine-version (push) Has been cancelled
CI and Release / push-image (push) Has been cancelled
CI and Release / push-verification-image (12) (push) Has been cancelled
CI and Release / lint-backend (push) Has been cancelled
CI and Release / push-verification-image (17) (push) Has been cancelled
CI and Release / push-verification-image (18) (push) Has been cancelled
CI and Release / release (push) Has been cancelled
CI and Release / publish-helm-chart (push) Has been cancelled
CI and Release / push-verification-image (13) (push) Has been cancelled
CI and Release / push-verification-image (14) (push) Has been cancelled
CI and Release / push-verification-image (15) (push) Has been cancelled
CI and Release / push-verification-image (16) (push) Has been cancelled
424 lines
14 KiB
Go
424 lines
14 KiB
Go
package verification_agents
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
users_enums "databasus-backend/internal/features/users/enums"
|
|
users_testing "databasus-backend/internal/features/users/testing"
|
|
"databasus-backend/internal/storage"
|
|
test_utils "databasus-backend/internal/util/testing"
|
|
)
|
|
|
|
func Test_Heartbeat_WhenTokenIsValid_UpdatesCapacityAndLastSeen(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
|
|
|
|
createdAgent := CreateTestVerificationAgent(t, router, admin.Token, "hb-ok-"+uuid.New().String())
|
|
|
|
beforeHeartbeat := time.Now().UTC()
|
|
|
|
var heartbeatResponse HeartbeatResponse
|
|
test_utils.MakePostRequestAndUnmarshal(
|
|
t, router,
|
|
heartbeatPathFor(createdAgent.Agent.ID),
|
|
"Bearer "+createdAgent.Token,
|
|
HeartbeatRequest{MaxCPU: 8, MaxRAMGb: 32, MaxDiskGb: 500, MaxConcurrentJobs: 4},
|
|
http.StatusOK, &heartbeatResponse,
|
|
)
|
|
|
|
assert.WithinDuration(t, time.Now().UTC(), heartbeatResponse.LastSeenAt, 10*time.Second)
|
|
assert.True(t, !heartbeatResponse.LastSeenAt.Before(beforeHeartbeat))
|
|
|
|
var listedAgents []Agent
|
|
test_utils.MakeGetRequestAndUnmarshal(
|
|
t, router,
|
|
"/api/v1/verification/agents",
|
|
"Bearer "+admin.Token,
|
|
http.StatusOK, &listedAgents,
|
|
)
|
|
|
|
var persistedAgent *Agent
|
|
for i := range listedAgents {
|
|
if listedAgents[i].ID == createdAgent.Agent.ID {
|
|
persistedAgent = &listedAgents[i]
|
|
}
|
|
}
|
|
require.NotNil(t, persistedAgent)
|
|
assert.Equal(t, 8, persistedAgent.MaxCPU)
|
|
assert.Equal(t, 32, persistedAgent.MaxRAMGb)
|
|
assert.Equal(t, 500, persistedAgent.MaxDiskGb)
|
|
assert.Equal(t, 4, persistedAgent.MaxConcurrentJobs)
|
|
require.NotNil(t, persistedAgent.LastSeenAt)
|
|
assert.WithinDuration(t, time.Now().UTC(), *persistedAgent.LastSeenAt, 10*time.Second)
|
|
}
|
|
|
|
func Test_Heartbeat_TwiceUpdatesSameRow(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
|
|
|
|
createdAgent := CreateTestVerificationAgent(t, router, admin.Token, "hb-twice-"+uuid.New().String())
|
|
heartbeatPath := heartbeatPathFor(createdAgent.Agent.ID)
|
|
|
|
var firstHeartbeat HeartbeatResponse
|
|
test_utils.MakePostRequestAndUnmarshal(
|
|
t, router, heartbeatPath, "Bearer "+createdAgent.Token,
|
|
HeartbeatRequest{MaxCPU: 1}, http.StatusOK, &firstHeartbeat,
|
|
)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
var secondHeartbeat HeartbeatResponse
|
|
test_utils.MakePostRequestAndUnmarshal(
|
|
t, router, heartbeatPath, "Bearer "+createdAgent.Token,
|
|
HeartbeatRequest{MaxCPU: 2}, http.StatusOK, &secondHeartbeat,
|
|
)
|
|
|
|
assert.True(t, secondHeartbeat.LastSeenAt.After(firstHeartbeat.LastSeenAt) ||
|
|
secondHeartbeat.LastSeenAt.Equal(firstHeartbeat.LastSeenAt))
|
|
|
|
var listedAgents []Agent
|
|
test_utils.MakeGetRequestAndUnmarshal(
|
|
t, router, "/api/v1/verification/agents",
|
|
"Bearer "+admin.Token, http.StatusOK, &listedAgents,
|
|
)
|
|
|
|
matchCount := 0
|
|
for _, agent := range listedAgents {
|
|
if agent.ID == createdAgent.Agent.ID {
|
|
matchCount++
|
|
assert.Equal(t, 2, agent.MaxCPU)
|
|
}
|
|
}
|
|
assert.Equal(t, 1, matchCount)
|
|
}
|
|
|
|
func Test_Heartbeat_PersistsRamAsGb_NotMb(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
|
|
|
|
createdAgent := CreateTestVerificationAgent(t, router, admin.Token, "hb-gb-"+uuid.New().String())
|
|
|
|
test_utils.MakePostRequest(
|
|
t, router, heartbeatPathFor(createdAgent.Agent.ID), "Bearer "+createdAgent.Token,
|
|
HeartbeatRequest{MaxRAMGb: 64}, http.StatusOK,
|
|
)
|
|
|
|
listResponse := test_utils.MakeGetRequest(
|
|
t, router, "/api/v1/verification/agents",
|
|
"Bearer "+admin.Token, http.StatusOK,
|
|
)
|
|
|
|
listBody := string(listResponse.Body)
|
|
assert.Contains(t, listBody, `"maxRamGb":64`)
|
|
assert.NotContains(t, listBody, "maxRamMb")
|
|
}
|
|
|
|
func Test_Heartbeat_WhenNoToken_ReturnsUnauthorized(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
|
|
|
|
createdAgent := CreateTestVerificationAgent(t, router, admin.Token, "hb-notoken-"+uuid.New().String())
|
|
|
|
heartbeatResponse := test_utils.MakePostRequest(
|
|
t, router, heartbeatPathFor(createdAgent.Agent.ID), "",
|
|
HeartbeatRequest{}, http.StatusUnauthorized,
|
|
)
|
|
assert.Contains(t, string(heartbeatResponse.Body), "invalid agent credentials")
|
|
}
|
|
|
|
func Test_Heartbeat_WhenInvalidToken_ReturnsUnauthorized(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
|
|
|
|
createdAgent := CreateTestVerificationAgent(t, router, admin.Token, "hb-bad-"+uuid.New().String())
|
|
|
|
heartbeatResponse := test_utils.MakePostRequest(
|
|
t, router, heartbeatPathFor(createdAgent.Agent.ID),
|
|
"Bearer "+uuid.New().String(),
|
|
HeartbeatRequest{}, http.StatusUnauthorized,
|
|
)
|
|
assert.Contains(t, string(heartbeatResponse.Body), "invalid agent credentials")
|
|
}
|
|
|
|
func Test_Heartbeat_WhenUserJwtInsteadOfAgentToken_ReturnsUnauthorized(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
|
|
|
|
createdAgent := CreateTestVerificationAgent(t, router, admin.Token, "hb-jwt-"+uuid.New().String())
|
|
|
|
test_utils.MakePostRequest(
|
|
t, router, heartbeatPathFor(createdAgent.Agent.ID), "Bearer "+admin.Token,
|
|
HeartbeatRequest{}, http.StatusUnauthorized,
|
|
)
|
|
}
|
|
|
|
func Test_Heartbeat_WhenAgentIdInvalidUuid_ReturnsUnauthorized(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
|
|
heartbeatResponse := test_utils.MakePostRequest(
|
|
t, router, "/api/v1/agent/verification/not-a-uuid/heartbeat",
|
|
"Bearer "+uuid.New().String(),
|
|
HeartbeatRequest{}, http.StatusUnauthorized,
|
|
)
|
|
assert.Contains(t, string(heartbeatResponse.Body), "invalid agent credentials")
|
|
}
|
|
|
|
func Test_Heartbeat_WhenAgentIdUnknown_ReturnsUnauthorized(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
|
|
heartbeatResponse := test_utils.MakePostRequest(
|
|
t, router, heartbeatPathFor(uuid.New()),
|
|
"Bearer "+uuid.New().String(),
|
|
HeartbeatRequest{}, http.StatusUnauthorized,
|
|
)
|
|
assert.Contains(t, string(heartbeatResponse.Body), "invalid agent credentials")
|
|
}
|
|
|
|
func Test_Heartbeat_WhenAgentIdMismatchesToken_ReturnsUnauthorized(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
|
|
|
|
agentA := CreateTestVerificationAgent(t, router, admin.Token, "hb-mix-a-"+uuid.New().String())
|
|
agentB := CreateTestVerificationAgent(t, router, admin.Token, "hb-mix-b-"+uuid.New().String())
|
|
|
|
test_utils.MakePostRequest(
|
|
t, router, heartbeatPathFor(agentA.Agent.ID), "Bearer "+agentA.Token,
|
|
HeartbeatRequest{MaxCPU: 1}, http.StatusOK,
|
|
)
|
|
|
|
var lastSeenBeforeMismatch *time.Time
|
|
storage.GetDb().Raw(`SELECT last_seen_at FROM verification_agents WHERE id = ?`,
|
|
agentA.Agent.ID).Scan(&lastSeenBeforeMismatch)
|
|
require.NotNil(t, lastSeenBeforeMismatch)
|
|
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
heartbeatResponse := test_utils.MakePostRequest(
|
|
t, router, heartbeatPathFor(agentA.Agent.ID), "Bearer "+agentB.Token,
|
|
HeartbeatRequest{MaxCPU: 99}, http.StatusUnauthorized,
|
|
)
|
|
assert.Contains(t, string(heartbeatResponse.Body), "invalid agent credentials")
|
|
|
|
var lastSeenAfterMismatch *time.Time
|
|
storage.GetDb().Raw(`SELECT last_seen_at FROM verification_agents WHERE id = ?`,
|
|
agentA.Agent.ID).Scan(&lastSeenAfterMismatch)
|
|
require.NotNil(t, lastSeenAfterMismatch)
|
|
assert.True(t, lastSeenAfterMismatch.Equal(*lastSeenBeforeMismatch),
|
|
"mismatched-token heartbeat must not update last_seen_at")
|
|
|
|
var persistedMaxCPU int
|
|
storage.GetDb().Raw(`SELECT max_cpu FROM verification_agents WHERE id = ?`,
|
|
agentA.Agent.ID).Scan(&persistedMaxCPU)
|
|
assert.Equal(t, 1, persistedMaxCPU)
|
|
}
|
|
|
|
func Test_Heartbeat_AfterRotateToken_OldTokenReturnsUnauthorized(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
|
|
|
|
createdAgent := CreateTestVerificationAgent(t, router, admin.Token, "hb-rotate-"+uuid.New().String())
|
|
originalToken := createdAgent.Token
|
|
|
|
var rotation RotateTokenResponse
|
|
test_utils.MakePostRequestAndUnmarshal(
|
|
t, router,
|
|
fmt.Sprintf("/api/v1/verification/agents/%s/rotate-token", createdAgent.Agent.ID),
|
|
"Bearer "+admin.Token,
|
|
map[string]any{},
|
|
http.StatusOK, &rotation,
|
|
)
|
|
|
|
test_utils.MakePostRequest(
|
|
t, router, heartbeatPathFor(createdAgent.Agent.ID), "Bearer "+originalToken,
|
|
HeartbeatRequest{}, http.StatusUnauthorized,
|
|
)
|
|
|
|
test_utils.MakePostRequest(
|
|
t, router, heartbeatPathFor(createdAgent.Agent.ID), "Bearer "+rotation.Token,
|
|
HeartbeatRequest{}, http.StatusOK,
|
|
)
|
|
}
|
|
|
|
func Test_Heartbeat_AfterDelete_ReturnsUnauthorized(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
|
|
|
|
createdAgent := CreateTestVerificationAgent(t, router, admin.Token, "hb-deleted-"+uuid.New().String())
|
|
|
|
test_utils.MakeDeleteRequest(
|
|
t, router,
|
|
fmt.Sprintf("/api/v1/verification/agents/%s", createdAgent.Agent.ID),
|
|
"Bearer "+admin.Token,
|
|
http.StatusNoContent,
|
|
)
|
|
|
|
heartbeatResponse := test_utils.MakePostRequest(
|
|
t, router, heartbeatPathFor(createdAgent.Agent.ID), "Bearer "+createdAgent.Token,
|
|
HeartbeatRequest{}, http.StatusUnauthorized,
|
|
)
|
|
assert.Contains(t, string(heartbeatResponse.Body), "invalid agent credentials")
|
|
}
|
|
|
|
func Test_Heartbeat_AfterDelete_DoesNotUpdateLastSeen(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
|
|
|
|
createdAgent := CreateTestVerificationAgent(t, router, admin.Token, "hb-no-update-"+uuid.New().String())
|
|
heartbeatPath := heartbeatPathFor(createdAgent.Agent.ID)
|
|
|
|
test_utils.MakePostRequest(
|
|
t, router, heartbeatPath, "Bearer "+createdAgent.Token,
|
|
HeartbeatRequest{MaxCPU: 1}, http.StatusOK,
|
|
)
|
|
|
|
var lastSeenBeforeDelete *time.Time
|
|
storage.GetDb().Raw(`SELECT last_seen_at FROM verification_agents WHERE id = ?`,
|
|
createdAgent.Agent.ID).Scan(&lastSeenBeforeDelete)
|
|
require.NotNil(t, lastSeenBeforeDelete)
|
|
|
|
test_utils.MakeDeleteRequest(
|
|
t, router,
|
|
fmt.Sprintf("/api/v1/verification/agents/%s", createdAgent.Agent.ID),
|
|
"Bearer "+admin.Token,
|
|
http.StatusNoContent,
|
|
)
|
|
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
test_utils.MakePostRequest(
|
|
t, router, heartbeatPath, "Bearer "+createdAgent.Token,
|
|
HeartbeatRequest{MaxCPU: 99}, http.StatusUnauthorized,
|
|
)
|
|
|
|
var lastSeenAfterDelete *time.Time
|
|
storage.GetDb().Raw(`SELECT last_seen_at FROM verification_agents WHERE id = ?`,
|
|
createdAgent.Agent.ID).Scan(&lastSeenAfterDelete)
|
|
require.NotNil(t, lastSeenAfterDelete)
|
|
assert.True(t, lastSeenAfterDelete.Equal(*lastSeenBeforeDelete),
|
|
"heartbeat against a soft-deleted agent must not update last_seen_at")
|
|
|
|
var persistedMaxCPU int
|
|
storage.GetDb().Raw(`SELECT max_cpu FROM verification_agents WHERE id = ?`,
|
|
createdAgent.Agent.ID).Scan(&persistedMaxCPU)
|
|
assert.Equal(t, 1, persistedMaxCPU,
|
|
"heartbeat against a soft-deleted agent must not update capacity")
|
|
}
|
|
|
|
func Test_Heartbeat_RejectsNegativeCapacity(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
|
|
|
|
createdAgent := CreateTestVerificationAgent(t, router, admin.Token, "hb-neg-"+uuid.New().String())
|
|
|
|
test_utils.MakePostRequest(
|
|
t, router, heartbeatPathFor(createdAgent.Agent.ID), "Bearer "+createdAgent.Token,
|
|
map[string]any{"maxCpu": -1}, http.StatusBadRequest,
|
|
)
|
|
}
|
|
|
|
func Test_Heartbeat_OverPerAgentRateLimit_Returns429(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
|
|
|
|
createdAgent := CreateTestVerificationAgent(t, router, admin.Token, "hb-burst-"+uuid.New().String())
|
|
heartbeatPath := heartbeatPathFor(createdAgent.Agent.ID)
|
|
|
|
for range rateLimitAgentMax {
|
|
response := test_utils.MakeRequest(t, router, test_utils.RequestOptions{
|
|
Method: http.MethodPost,
|
|
URL: heartbeatPath,
|
|
AuthToken: "Bearer " + createdAgent.Token,
|
|
Body: HeartbeatRequest{MaxCPU: 1},
|
|
})
|
|
require.Equal(t, http.StatusOK, response.StatusCode,
|
|
"requests within the per-agent budget should succeed")
|
|
}
|
|
|
|
response := test_utils.MakeRequest(t, router, test_utils.RequestOptions{
|
|
Method: http.MethodPost,
|
|
URL: heartbeatPath,
|
|
AuthToken: "Bearer " + createdAgent.Token,
|
|
Body: HeartbeatRequest{MaxCPU: 1},
|
|
})
|
|
assert.Equal(t, http.StatusTooManyRequests, response.StatusCode,
|
|
"the request past the per-agent budget should be rate-limited")
|
|
assert.Contains(t, string(response.Body), "too many requests")
|
|
}
|
|
|
|
func Test_Heartbeat_InvalidUuidDoesNotConsumeRateLimit(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
|
|
|
|
createdAgent := CreateTestVerificationAgent(t, router, admin.Token, "hb-garbage-"+uuid.New().String())
|
|
|
|
for range rateLimitAgentMax * 2 {
|
|
response := test_utils.MakeRequest(t, router, test_utils.RequestOptions{
|
|
Method: http.MethodPost,
|
|
URL: "/api/v1/agent/verification/not-a-uuid/heartbeat",
|
|
AuthToken: "Bearer " + createdAgent.Token,
|
|
Body: HeartbeatRequest{},
|
|
})
|
|
require.Equal(t, http.StatusUnauthorized, response.StatusCode,
|
|
"garbage-UUID requests must always reject as 401, never as 429")
|
|
}
|
|
|
|
test_utils.MakePostRequest(
|
|
t, router, heartbeatPathFor(createdAgent.Agent.ID), "Bearer "+createdAgent.Token,
|
|
HeartbeatRequest{MaxCPU: 1}, http.StatusOK,
|
|
)
|
|
}
|
|
|
|
func Test_AdminEndpoint_WithAgentToken_ReturnsUnauthorized(t *testing.T) {
|
|
cleanupAgents(t)
|
|
|
|
router := createTestRouter()
|
|
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
|
|
|
|
createdAgent := CreateTestVerificationAgent(t, router, admin.Token, "cross-"+uuid.New().String())
|
|
|
|
test_utils.MakePostRequest(
|
|
t, router,
|
|
"/api/v1/verification/agents",
|
|
"Bearer "+createdAgent.Token,
|
|
CreateAgentRequest{Name: "should-not-work"},
|
|
http.StatusUnauthorized,
|
|
)
|
|
}
|