First release of open core
This commit is contained in:
62
pkg/admin/admin.go
Normal file
62
pkg/admin/admin.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func (h *Handler) HandleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := h.Store.GetAppConfig(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to fetch configuration", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(config)
|
||||
}
|
||||
|
||||
func (h *Handler) HandleExportState(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := h.Store.ExportSystemState(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to generate system export", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=RiskRancher_export.json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(state); err != nil {
|
||||
// Note: We can't change the HTTP status code here because we've already started streaming,
|
||||
// but we can log the error if the stream breaks.
|
||||
_ = err
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) HandleGetLogs(w http.ResponseWriter, r *http.Request) {
|
||||
filter := r.URL.Query().Get("filter")
|
||||
page, err := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
limit := 15
|
||||
offset := (page - 1) * limit
|
||||
|
||||
feed, total, err := h.Store.GetPaginatedActivityFeed(r.Context(), filter, limit, offset)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to load logs", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"feed": feed,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
192
pkg/admin/admin_handlers.go
Normal file
192
pkg/admin/admin_handlers.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"epigas.gitea.cloud/RiskRancher/core/pkg/auth"
|
||||
)
|
||||
|
||||
// PasswordResetRequest is the expected JSON payload
|
||||
type PasswordResetRequest struct {
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
// HandleAdminResetPassword allows a Sheriff to forcefully overwrite a user's password.
|
||||
func (h *Handler) HandleAdminResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.PathValue("id")
|
||||
userID, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid user ID in URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req PasswordResetRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.NewPassword == "" {
|
||||
http.Error(w, "New password cannot be empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := auth.HashPassword(req.NewPassword)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error during hashing", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.Store.UpdateUserPassword(r.Context(), userID, hashedPassword)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to update user password", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": "Password reset successfully",
|
||||
})
|
||||
}
|
||||
|
||||
type RoleUpdateRequest struct {
|
||||
GlobalRole string `json:"global_role"`
|
||||
}
|
||||
|
||||
// HandleUpdateUserRole allows a Sheriff to promote or demote a user.
|
||||
func (h *Handler) HandleUpdateUserRole(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.PathValue("id")
|
||||
userID, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid user ID in URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var req RoleUpdateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
validRoles := map[string]bool{
|
||||
"Sheriff": true, "Wrangler": true, "RangeHand": true, "CircuitRider": true, "Magistrate": true,
|
||||
}
|
||||
if !validRoles[req.GlobalRole] {
|
||||
http.Error(w, "Invalid role provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.Store.UpdateUserRole(r.Context(), userID, req.GlobalRole)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to update user role", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": "User role updated successfully to " + req.GlobalRole,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleDeactivateUser allows a Sheriff to safely offboard a user.
|
||||
func (h *Handler) HandleDeactivateUser(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.PathValue("id")
|
||||
userID, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid user ID in URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.Store.DeactivateUserAndReassign(r.Context(), userID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to deactivate user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": "User successfully deactivated and tickets reassigned.",
|
||||
})
|
||||
}
|
||||
|
||||
// CreateUserRequest is the payload the Sheriff sends to invite a new user
|
||||
type CreateUserRequest struct {
|
||||
Email string `json:"email"`
|
||||
FullName string `json:"full_name"`
|
||||
Password string `json:"password"`
|
||||
GlobalRole string `json:"global_role"`
|
||||
}
|
||||
|
||||
// HandleCreateUser allows a Sheriff to manually provision a new user account.
|
||||
func (h *Handler) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req CreateUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Email == "" || req.FullName == "" || req.Password == "" || req.GlobalRole == "" {
|
||||
http.Error(w, "Missing required fields", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
validRoles := map[string]bool{
|
||||
"Sheriff": true, "Wrangler": true, "RangeHand": true, "CircuitRider": true, "Magistrate": true,
|
||||
}
|
||||
if !validRoles[req.GlobalRole] {
|
||||
http.Error(w, "Invalid role provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := auth.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error during hashing", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.Store.CreateUser(r.Context(), req.Email, req.FullName, hashedPassword, req.GlobalRole)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
http.Error(w, "Email already exists in the system", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Failed to provision user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"message": "User provisioned successfully. Share the temporary password securely.",
|
||||
"id": user.ID,
|
||||
"email": user.Email,
|
||||
"full_name": user.FullName,
|
||||
"global_role": user.GlobalRole,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleGetUsers returns a list of all users in the system for the Sheriff to manage.
|
||||
func (h *Handler) HandleGetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := h.Store.GetAllUsers(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to fetch user roster", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(users)
|
||||
}
|
||||
|
||||
// HandleGetWranglers returns a clean list of IT users for assignment dropdowns
|
||||
func (h *Handler) HandleGetWranglers(w http.ResponseWriter, r *http.Request) {
|
||||
wranglers, err := h.Store.GetWranglers(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to fetch wranglers", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(wranglers)
|
||||
}
|
||||
69
pkg/admin/admin_lifecycle.go
Normal file
69
pkg/admin/admin_lifecycle.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const CurrentAppVersion = "v1.0.0"
|
||||
|
||||
type UpdateCheckResponse struct {
|
||||
Status string `json:"status"`
|
||||
CurrentVersion string `json:"current_version"`
|
||||
LatestVersion string `json:"latest_version,omitempty"`
|
||||
UpdateAvailable bool `json:"update_available"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// HandleCheckUpdates pings gitea. If air-gapped, it returns manual instructions.
|
||||
func (h *Handler) HandleCheckUpdates(w http.ResponseWriter, r *http.Request) {
|
||||
respPayload := UpdateCheckResponse{
|
||||
CurrentVersion: CurrentAppVersion,
|
||||
}
|
||||
|
||||
client := http.Client{Timeout: 3 * time.Second}
|
||||
|
||||
giteaURL := "https://epigas.gitea.cloud/api/v1/repos/RiskRancher/core/releases/latest"
|
||||
resp, err := client.Get(giteaURL)
|
||||
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
respPayload.Status = "offline"
|
||||
respPayload.Message = "No internet connection detected. To update an air-gapped server: Download the latest RiskRancher binary on a connected machine, transfer it via rsync or scp to this server, and restart the service."
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(respPayload)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var ghRelease struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&ghRelease); err == nil {
|
||||
respPayload.Status = "online"
|
||||
respPayload.LatestVersion = ghRelease.TagName
|
||||
respPayload.UpdateAvailable = (ghRelease.TagName != CurrentAppVersion)
|
||||
|
||||
if respPayload.UpdateAvailable {
|
||||
respPayload.Message = "A new version is available! Please trigger a graceful shutdown and swap the binary."
|
||||
} else {
|
||||
respPayload.Message = "You are running the latest version."
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(respPayload)
|
||||
}
|
||||
|
||||
// HandleShutdown signals the application to close connections and exit cleanly
|
||||
func (h *Handler) HandleShutdown(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"message": "Initiating graceful shutdown. The server will exit in 2 seconds..."}`))
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
}()
|
||||
}
|
||||
64
pkg/admin/admin_test.go
Normal file
64
pkg/admin/admin_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"epigas.gitea.cloud/RiskRancher/core/pkg/domain"
|
||||
)
|
||||
|
||||
func TestGetGlobalConfig(t *testing.T) {
|
||||
app, db := setupTestAdmin(t)
|
||||
defer db.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/config", nil)
|
||||
req.AddCookie(GetVIPCookie(app.Store))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
app.HandleGetConfig(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("Expected 200 OK, got %d. Body: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
|
||||
var config domain.AppConfig
|
||||
if err := json.NewDecoder(rr.Body).Decode(&config); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if config.Timezone != "America/New_York" || config.BusinessStart != 9 {
|
||||
t.Errorf("Expected default config, got TZ: %s, Start: %d", config.Timezone, config.BusinessStart)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleDeactivateUser(t *testing.T) {
|
||||
h, db := setupTestAdmin(t)
|
||||
defer db.Close()
|
||||
|
||||
targetUser, _ := h.Store.CreateUser(context.Background(), "fired@ranch.com", "Fired Fred", "hash", "RangeHand")
|
||||
res, _ := db.Exec(`INSERT INTO tickets (title, status, severity, source, dedupe_hash) VALUES ('Freds Task', 'Waiting to be Triaged', 'High', 'Manual', 'fake-hash-123')`)
|
||||
ticketID, _ := res.LastInsertId()
|
||||
db.Exec(`INSERT INTO ticket_assignments (ticket_id, assignee, role) VALUES (?, 'fired@ranch.com', 'RangeHand')`, ticketID)
|
||||
|
||||
targetURL := fmt.Sprintf("/api/admin/users/%d", targetUser.ID)
|
||||
req := httptest.NewRequest(http.MethodDelete, targetURL, nil)
|
||||
req.AddCookie(GetVIPCookie(h.Store))
|
||||
req.SetPathValue("id", fmt.Sprintf("%d", targetUser.ID))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
h.HandleDeactivateUser(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("Expected 200 OK, got %d. Body: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
|
||||
var count int
|
||||
db.QueryRow(`SELECT COUNT(*) FROM ticket_assignments WHERE assignee = 'fired@ranch.com'`).Scan(&count)
|
||||
if count != 0 {
|
||||
t.Errorf("Expected assignments to be cleared, but found %d", count)
|
||||
}
|
||||
}
|
||||
106
pkg/admin/admin_users_test.go
Normal file
106
pkg/admin/admin_users_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandleAdminResetPassword(t *testing.T) {
|
||||
a, db := setupTestAdmin(t)
|
||||
defer db.Close()
|
||||
|
||||
targetUser, _ := a.Store.CreateUser(context.Background(), "forgetful@ranch.com", "Forgetful Fred", "old_hash", "RangeHand")
|
||||
|
||||
payload := map[string]string{
|
||||
"new_password": "BrandNewSecurePassword123!",
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
|
||||
targetURL := fmt.Sprintf("/api/admin/users/%d/reset-password", targetUser.ID)
|
||||
req := httptest.NewRequest(http.MethodPatch, targetURL, bytes.NewBuffer(body))
|
||||
|
||||
req.SetPathValue("id", fmt.Sprintf("%d", targetUser.ID))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.HandleAdminResetPassword(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("Expected 200 OK, got %d. Body: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateUserRole(t *testing.T) {
|
||||
a, db := setupTestAdmin(t)
|
||||
defer db.Close()
|
||||
|
||||
_, _ = a.Store.CreateUser(context.Background(), "boss@ranch.com", "The Boss", "hash", "Sheriff")
|
||||
targetUser, _ := a.Store.CreateUser(context.Background(), "rookie@ranch.com", "Rookie Ray", "hash", "RangeHand")
|
||||
|
||||
payload := map[string]string{
|
||||
"global_role": "Wrangler",
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
|
||||
targetURL := fmt.Sprintf("/api/admin/users/%d/role", targetUser.ID)
|
||||
req := httptest.NewRequest(http.MethodPatch, targetURL, bytes.NewBuffer(body))
|
||||
|
||||
req.AddCookie(GetVIPCookie(a.Store))
|
||||
req.SetPathValue("id", fmt.Sprintf("%d", targetUser.ID))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.HandleUpdateUserRole(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("Expected 200 OK, got %d. Body: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCreateUser_SheriffInvite(t *testing.T) {
|
||||
a, db := setupTestAdmin(t)
|
||||
defer db.Close()
|
||||
|
||||
payload := map[string]string{
|
||||
"email": "magistrate@ranch.com",
|
||||
"full_name": "Mighty Magistrate",
|
||||
"password": "TempPassword123!",
|
||||
"global_role": "Magistrate",
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/admin/users", bytes.NewBuffer(body))
|
||||
|
||||
req.AddCookie(GetVIPCookie(a.Store))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rr := httptest.NewRecorder()
|
||||
a.HandleCreateUser(rr, req)
|
||||
if rr.Code != http.StatusCreated {
|
||||
t.Fatalf("Expected 201 Created, got %d. Body: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
|
||||
var count int
|
||||
db.QueryRow(`SELECT COUNT(*) FROM users WHERE email = 'magistrate@ranch.com'`).Scan(&count)
|
||||
if count != 1 {
|
||||
t.Errorf("Expected user to be created in the database")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleGetUsers(t *testing.T) {
|
||||
a, db := setupTestAdmin(t)
|
||||
defer db.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/admin/users", nil)
|
||||
|
||||
req.AddCookie(GetVIPCookie(a.Store))
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.HandleGetUsers(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("Expected 200 OK, got %d. Body: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
44
pkg/admin/export_test.go
Normal file
44
pkg/admin/export_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"epigas.gitea.cloud/RiskRancher/core/pkg/domain"
|
||||
)
|
||||
|
||||
func TestExportSystemState(t *testing.T) {
|
||||
app, db := setupTestAdmin(t)
|
||||
defer db.Close()
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO tickets (title, severity, status, dedupe_hash)
|
||||
VALUES ('Export Test Vuln', 'High', 'Triaged', 'test_hash_123')
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert test ticket: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/admin/export", nil)
|
||||
req.AddCookie(GetVIPCookie(app.Store))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
app.HandleExportState(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("Expected 200 OK, got %d", rr.Code)
|
||||
}
|
||||
|
||||
if rr.Header().Get("Content-Disposition") != "attachment; filename=RiskRancher_export.json" {
|
||||
t.Errorf("Missing or incorrect Content-Disposition header")
|
||||
}
|
||||
|
||||
var state domain.ExportState
|
||||
if err := json.NewDecoder(rr.Body).Decode(&state); err != nil {
|
||||
t.Fatalf("Failed to decode exported JSON: %v", err)
|
||||
}
|
||||
|
||||
if len(state.Tickets) == 0 || state.Tickets[0].Title != "Export Test Vuln" {
|
||||
t.Errorf("Export did not contain the expected ticket data")
|
||||
}
|
||||
}
|
||||
15
pkg/admin/handler.go
Normal file
15
pkg/admin/handler.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"epigas.gitea.cloud/RiskRancher/core/pkg/domain"
|
||||
)
|
||||
|
||||
// Handler encapsulates all Admin and Sheriff HTTP logic
|
||||
type Handler struct {
|
||||
Store domain.Store
|
||||
}
|
||||
|
||||
// NewHandler creates a new Admin Handler
|
||||
func NewHandler(store domain.Store) *Handler {
|
||||
return &Handler{Store: store}
|
||||
}
|
||||
30
pkg/admin/helpers_test.go
Normal file
30
pkg/admin/helpers_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"epigas.gitea.cloud/RiskRancher/core/pkg/datastore"
|
||||
"epigas.gitea.cloud/RiskRancher/core/pkg/domain"
|
||||
)
|
||||
|
||||
// setupTestAdmin returns the clean Admin Handler and the raw DB
|
||||
func setupTestAdmin(t *testing.T) (*Handler, *sql.DB) {
|
||||
db := datastore.InitDB(":memory:")
|
||||
store := datastore.NewSQLiteStore(db)
|
||||
return NewHandler(store), db
|
||||
}
|
||||
|
||||
// GetVIPCookie creates a dummy Sheriff user to bypass the Bouncer in tests
|
||||
func GetVIPCookie(store domain.Store) *http.Cookie {
|
||||
user, err := store.GetUserByEmail(context.Background(), "vip_test@RiskRancher.com")
|
||||
if err != nil {
|
||||
user, _ = store.CreateUser(context.Background(), "vip_test@RiskRancher.com", "Test VIP", "hash", "Sheriff")
|
||||
}
|
||||
token := "vip_test_token_999"
|
||||
store.CreateSession(context.Background(), token, user.ID, time.Now().Add(1*time.Hour))
|
||||
return &http.Cookie{Name: "session_token", Value: token}
|
||||
}
|
||||
36
pkg/admin/updates_test.go
Normal file
36
pkg/admin/updates_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheckUpdates_OfflineFallback(t *testing.T) {
|
||||
|
||||
app, db := setupTestAdmin(t)
|
||||
defer db.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/admin/check-updates", nil)
|
||||
req.AddCookie(GetVIPCookie(app.Store))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
app.HandleCheckUpdates(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("Expected 200 OK, got %d", rr.Code)
|
||||
}
|
||||
|
||||
var response map[string]interface{}
|
||||
if err := json.NewDecoder(rr.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if _, exists := response["status"]; !exists {
|
||||
t.Errorf("Expected 'status' field in response")
|
||||
}
|
||||
if _, exists := response["message"]; !exists {
|
||||
t.Errorf("Expected 'message' field in response")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user