Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:11:07 +08:00

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,
)
}