215 lines
6.1 KiB
Go
215 lines
6.1 KiB
Go
// ©AngelaMos | 2026
|
|
// integration_test.go
|
|
|
|
//go:build integration
|
|
|
|
package admin_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/admin"
|
|
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/event"
|
|
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/middleware"
|
|
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/testutil"
|
|
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token"
|
|
)
|
|
|
|
const (
|
|
intgBaseURL = "https://canary.example.com"
|
|
intgManageURL = "https://canary.example.com"
|
|
intgOperatorToken = "test-operator-token-CONSTANT-COMPARE-OK"
|
|
)
|
|
|
|
type intgURLBuilder struct{}
|
|
|
|
func (intgURLBuilder) TriggerURL(id string) string {
|
|
return intgBaseURL + "/c/" + id
|
|
}
|
|
|
|
func (intgURLBuilder) ManageURL(manageID string) string {
|
|
return intgManageURL + "/m/" + manageID
|
|
}
|
|
|
|
type intgStack struct {
|
|
router chi.Router
|
|
tokenRepo *token.Repository
|
|
eventRepo *event.Repository
|
|
}
|
|
|
|
func setupIntgStack(t *testing.T) *intgStack {
|
|
t.Helper()
|
|
db := sqlx.NewDb(testutil.NewTestDB(t), "pgx")
|
|
|
|
tokenRepo := token.NewRepository(db)
|
|
eventRepo := event.NewRepository(db)
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
|
|
adminH := admin.NewHandler(tokenRepo, eventRepo, intgURLBuilder{}, logger)
|
|
|
|
r := chi.NewRouter()
|
|
r.Route("/api", func(api chi.Router) {
|
|
api.Route("/admin", func(adm chi.Router) {
|
|
adm.Use(middleware.OperatorBearer(intgOperatorToken))
|
|
adminH.RegisterRoutes(adm)
|
|
})
|
|
})
|
|
|
|
return &intgStack{router: r, tokenRepo: tokenRepo, eventRepo: eventRepo}
|
|
}
|
|
|
|
func seedIntgToken(
|
|
t *testing.T,
|
|
repo *token.Repository,
|
|
id string,
|
|
typ token.Type,
|
|
ch token.AlertChannel,
|
|
) *token.Token {
|
|
t.Helper()
|
|
tok := &token.Token{
|
|
ID: id,
|
|
ManageID: uuid.New().String(),
|
|
Type: typ,
|
|
Memo: "admin-intg",
|
|
AlertChannel: ch,
|
|
CreatedIP: "203.0.113.42",
|
|
CreatedFP: "abcdef0123456789",
|
|
Metadata: json.RawMessage(`{}`),
|
|
Enabled: true,
|
|
}
|
|
switch ch {
|
|
case token.ChannelTelegram:
|
|
tok.TelegramBot = testutil.Ptr("111:AAA")
|
|
tok.TelegramChat = testutil.Ptr("12345")
|
|
case token.ChannelWebhook:
|
|
tok.WebhookURL = testutil.Ptr("https://example.com/hook")
|
|
}
|
|
require.NoError(t, repo.Insert(context.Background(), tok))
|
|
return tok
|
|
}
|
|
|
|
func authedGet(t *testing.T, r chi.Router, path, tok string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
if tok != "" {
|
|
req.Header.Set("Authorization", "Bearer "+tok)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
|
|
func authedPost(t *testing.T, r chi.Router, path, tok string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodPost, path, nil)
|
|
if tok != "" {
|
|
req.Header.Set("Authorization", "Bearer "+tok)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
|
|
func TestIntegration_AdminBearer_404OnMissing(t *testing.T) {
|
|
st := setupIntgStack(t)
|
|
w := authedGet(t, st.router, "/api/admin/stats", "")
|
|
require.Equal(t, http.StatusNotFound, w.Code,
|
|
"missing Authorization must return 404, not 401")
|
|
require.Empty(t, w.Header().Get("WWW-Authenticate"))
|
|
}
|
|
|
|
func TestIntegration_AdminBearer_404OnWrongToken(t *testing.T) {
|
|
st := setupIntgStack(t)
|
|
w := authedGet(t, st.router, "/api/admin/stats", "wrong-token")
|
|
require.Equal(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func TestIntegration_AdminStats_Authorized(t *testing.T) {
|
|
st := setupIntgStack(t)
|
|
ctx := context.Background()
|
|
|
|
tok := seedIntgToken(t, st.tokenRepo, "adminstats01", token.TypeWebbug, token.ChannelTelegram)
|
|
seedIntgToken(t, st.tokenRepo, "adminstats02", token.TypeDocx, token.ChannelWebhook)
|
|
|
|
require.NoError(t, st.eventRepo.Insert(ctx, &event.Event{
|
|
TokenID: tok.ID, SourceIP: "203.0.113.50",
|
|
}))
|
|
|
|
w := authedGet(t, st.router, "/api/admin/stats", intgOperatorToken)
|
|
require.Equal(t, http.StatusOK, w.Code, "body=%s", w.Body.String())
|
|
|
|
var body struct {
|
|
Success bool `json:"success"`
|
|
Data admin.Stats `json:"data"`
|
|
}
|
|
require.NoError(t, json.NewDecoder(w.Body).Decode(&body))
|
|
require.True(t, body.Success)
|
|
require.GreaterOrEqual(t, body.Data.TokensCount, int64(2))
|
|
require.GreaterOrEqual(t, body.Data.EventsCount, int64(1))
|
|
require.NotEmpty(t, body.Data.ByType)
|
|
require.NotEmpty(t, body.Data.ByAlertChannel)
|
|
}
|
|
|
|
func TestIntegration_AdminListTokens_Authorized(t *testing.T) {
|
|
st := setupIntgStack(t)
|
|
|
|
for i := range 4 {
|
|
seedIntgToken(t, st.tokenRepo,
|
|
"adminlist0"+string(rune('a'+i)),
|
|
token.TypeWebbug, token.ChannelWebhook)
|
|
}
|
|
|
|
w := authedGet(t, st.router, "/api/admin/tokens?limit=2", intgOperatorToken)
|
|
require.Equal(t, http.StatusOK, w.Code, "body=%s", w.Body.String())
|
|
|
|
var body struct {
|
|
Data admin.TokenListResponse `json:"data"`
|
|
}
|
|
require.NoError(t, json.NewDecoder(w.Body).Decode(&body))
|
|
require.Len(t, body.Data.Tokens, 2)
|
|
require.GreaterOrEqual(t, body.Data.Total, int64(4))
|
|
require.True(t, body.Data.Page.HasMore)
|
|
require.Equal(t, 2, body.Data.Page.NextOffset)
|
|
|
|
require.Contains(t, body.Data.Tokens[0].TriggerURL, "/c/")
|
|
require.Contains(t, body.Data.Tokens[0].ManageURL, "/m/")
|
|
}
|
|
|
|
func TestIntegration_AdminDisableToken_Authorized(t *testing.T) {
|
|
st := setupIntgStack(t)
|
|
ctx := context.Background()
|
|
|
|
tok := seedIntgToken(t, st.tokenRepo, "admindis001a", token.TypeWebbug, token.ChannelWebhook)
|
|
require.True(t, tok.Enabled)
|
|
|
|
w := authedPost(t, st.router,
|
|
"/api/admin/tokens/"+tok.ID+"/disable",
|
|
intgOperatorToken)
|
|
require.Equal(t, http.StatusNoContent, w.Code)
|
|
|
|
got, err := st.tokenRepo.GetByID(ctx, tok.ID)
|
|
require.NoError(t, err)
|
|
require.False(t, got.Enabled, "disable must persist to DB")
|
|
}
|
|
|
|
func TestIntegration_AdminDisableToken_NotFound(t *testing.T) {
|
|
st := setupIntgStack(t)
|
|
|
|
w := authedPost(t, st.router,
|
|
"/api/admin/tokens/admindismiss/disable",
|
|
intgOperatorToken)
|
|
require.Equal(t, http.StatusNotFound, w.Code)
|
|
require.Contains(t, w.Body.String(), `"NOT_FOUND"`)
|
|
}
|