Merged in feature/eula.part1 (pull request #206)
eula support * eula support * docs
This commit is contained in:
@@ -36,9 +36,10 @@ func getAdminUserForAudit(ctx echo.Context) (cognitoauth.UserInfo, error) {
|
||||
}
|
||||
|
||||
// If auth is disabled, use a default system user for audit logging
|
||||
// Note: Email must be a valid format (with TLD) to pass openapi_types.Email validation
|
||||
if os.Getenv("DISABLE_AUTH") == "true" {
|
||||
return cognitoauth.UserInfo{
|
||||
Email: "system@localhost",
|
||||
Email: "system@test.local",
|
||||
Username: "system",
|
||||
Groups: []string{"admin"},
|
||||
}, nil
|
||||
|
||||
+814
-214
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import (
|
||||
"queryorchestration/internal/document"
|
||||
documentbatch "queryorchestration/internal/document/batch"
|
||||
documentupload "queryorchestration/internal/document/upload"
|
||||
"queryorchestration/internal/eula"
|
||||
"queryorchestration/internal/export"
|
||||
"queryorchestration/internal/fieldextraction"
|
||||
"queryorchestration/internal/folder"
|
||||
@@ -33,6 +34,7 @@ type Services struct {
|
||||
FieldExtraction *fieldextraction.Service
|
||||
Folder *folder.Service
|
||||
Label *label.Service
|
||||
Eula *eula.Service
|
||||
}
|
||||
|
||||
// ConfigProvider combines auth and objectstore interfaces for the Controllers
|
||||
|
||||
@@ -0,0 +1,520 @@
|
||||
// eulaAdminHandlers.go implements the admin EULA API handlers.
|
||||
// These endpoints handle EULA version management, agreement listing, and compliance reporting.
|
||||
package queryapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"queryorchestration/internal/database/repository"
|
||||
"queryorchestration/internal/eula"
|
||||
"queryorchestration/internal/serviceconfig/aws"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/oapi-codegen/nullable"
|
||||
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||
)
|
||||
|
||||
// ListEulaVersions returns a paginated list of all EULA versions.
|
||||
// GET /admin/eula
|
||||
//
|
||||
// Requires AdminService permission.
|
||||
//
|
||||
// Parameters:
|
||||
// - page: Page number (1-based, default: 1)
|
||||
// - page_size: Items per page (1-100, default: 20)
|
||||
//
|
||||
// Returns:
|
||||
// - 200 OK with EulaVersionList on success
|
||||
// - 401 Unauthorized if not authenticated
|
||||
// - 403 Forbidden if not authorized
|
||||
// - 500 Internal Server Error on database errors
|
||||
func (s *Controllers) ListEulaVersions(ctx echo.Context, params ListEulaVersionsParams) error {
|
||||
// Apply defaults
|
||||
page := int32(1)
|
||||
pageSize := int32(20)
|
||||
if params.Page != nil {
|
||||
page = *params.Page
|
||||
}
|
||||
if params.PageSize != nil {
|
||||
pageSize = *params.PageSize
|
||||
}
|
||||
|
||||
result, err := s.svc.Eula.ListVersions(ctx.Request().Context(), &eula.ListVersionsInput{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
||||
Message: "Failed to list EULA versions",
|
||||
})
|
||||
}
|
||||
|
||||
// Convert to API response
|
||||
versions := make([]EulaVersion, len(result.Versions))
|
||||
for i, v := range result.Versions {
|
||||
versions[i] = convertVersionToAPI(v)
|
||||
}
|
||||
|
||||
response := EulaVersionList{
|
||||
Versions: versions,
|
||||
Total: int32(result.Total), // #nosec G115 -- Total is bounded by practical EULA version counts (< 1000)
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
HasMore: &result.HasMore,
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// CreateEulaVersion creates a new EULA version.
|
||||
// POST /admin/eula
|
||||
//
|
||||
// Requires AdminService permission.
|
||||
//
|
||||
// Request body: EulaVersionCreate
|
||||
//
|
||||
// Returns:
|
||||
// - 201 Created with EulaVersion on success
|
||||
// - 400 Bad Request on validation errors
|
||||
// - 401 Unauthorized if not authenticated
|
||||
// - 403 Forbidden if not authorized
|
||||
// - 409 Conflict if version string already exists
|
||||
// - 500 Internal Server Error on database errors
|
||||
func (s *Controllers) CreateEulaVersion(ctx echo.Context) error {
|
||||
var req EulaVersionCreate
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
|
||||
Message: "Invalid request body",
|
||||
})
|
||||
}
|
||||
|
||||
// Get admin user for audit
|
||||
adminUser, err := getAdminUserForAudit(ctx)
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
|
||||
Message: "Could not identify admin user",
|
||||
})
|
||||
}
|
||||
|
||||
// Extract effectiveDate if specified (nullable field)
|
||||
var effectiveDate *time.Time
|
||||
if req.EffectiveDate.IsSpecified() && !req.EffectiveDate.IsNull() {
|
||||
val := req.EffectiveDate.MustGet()
|
||||
effectiveDate = &val
|
||||
}
|
||||
|
||||
version, err := s.svc.Eula.CreateVersion(ctx.Request().Context(), &eula.CreateVersionInput{
|
||||
Version: req.Version,
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
EffectiveDate: effectiveDate,
|
||||
CreatedBy: adminUser.Email,
|
||||
})
|
||||
if err != nil {
|
||||
// Check for duplicate version
|
||||
if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "unique") {
|
||||
return ctx.JSON(http.StatusConflict, ErrorMessage{
|
||||
Message: "EULA version already exists",
|
||||
})
|
||||
}
|
||||
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
||||
Message: "Failed to create EULA version",
|
||||
})
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusCreated, convertVersionToAPI(version))
|
||||
}
|
||||
|
||||
// GetEulaVersion retrieves a specific EULA version by ID.
|
||||
// GET /admin/eula/{version_id}
|
||||
//
|
||||
// Requires AdminService permission.
|
||||
//
|
||||
// Returns:
|
||||
// - 200 OK with EulaVersion on success
|
||||
// - 401 Unauthorized if not authenticated
|
||||
// - 403 Forbidden if not authorized
|
||||
// - 404 Not Found if version doesn't exist
|
||||
// - 500 Internal Server Error on database errors
|
||||
func (s *Controllers) GetEulaVersion(ctx echo.Context, versionID EulaVersionID) error {
|
||||
id, err := convertVersionIDParam(versionID)
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
|
||||
Message: "Invalid version ID format",
|
||||
})
|
||||
}
|
||||
|
||||
version, err := s.svc.Eula.GetVersionByID(ctx.Request().Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, eula.ErrVersionNotFound) {
|
||||
return ctx.JSON(http.StatusNotFound, ErrorMessage{
|
||||
Message: "EULA version not found",
|
||||
})
|
||||
}
|
||||
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
||||
Message: "Failed to get EULA version",
|
||||
})
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, convertVersionToAPI(version))
|
||||
}
|
||||
|
||||
// UpdateEulaVersion updates the metadata of an EULA version.
|
||||
// Only title and effective date can be updated; content is immutable.
|
||||
// PATCH /admin/eula/{version_id}
|
||||
//
|
||||
// Requires AdminService permission.
|
||||
//
|
||||
// Request body: EulaVersionUpdate
|
||||
//
|
||||
// Returns:
|
||||
// - 200 OK with updated EulaVersion on success
|
||||
// - 400 Bad Request on validation errors
|
||||
// - 401 Unauthorized if not authenticated
|
||||
// - 403 Forbidden if not authorized
|
||||
// - 404 Not Found if version doesn't exist
|
||||
// - 500 Internal Server Error on database errors
|
||||
func (s *Controllers) UpdateEulaVersion(ctx echo.Context, versionID EulaVersionID) error {
|
||||
id, err := convertVersionIDParam(versionID)
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
|
||||
Message: "Invalid version ID format",
|
||||
})
|
||||
}
|
||||
|
||||
var req EulaVersionUpdate
|
||||
if err := ctx.Bind(&req); err != nil {
|
||||
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
|
||||
Message: "Invalid request body",
|
||||
})
|
||||
}
|
||||
|
||||
input := &eula.UpdateVersionInput{}
|
||||
if req.Title != nil {
|
||||
input.Title = req.Title
|
||||
}
|
||||
if req.EffectiveDate != nil {
|
||||
input.EffectiveDate = req.EffectiveDate
|
||||
}
|
||||
|
||||
version, err := s.svc.Eula.UpdateVersion(ctx.Request().Context(), id, input)
|
||||
if err != nil {
|
||||
if errors.Is(err, eula.ErrVersionNotFound) {
|
||||
return ctx.JSON(http.StatusNotFound, ErrorMessage{
|
||||
Message: "EULA version not found",
|
||||
})
|
||||
}
|
||||
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
||||
Message: "Failed to update EULA version",
|
||||
})
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, convertVersionToAPI(version))
|
||||
}
|
||||
|
||||
// ActivateEulaVersion sets the specified version as the current active EULA.
|
||||
// POST /admin/eula/{version_id}/activate
|
||||
//
|
||||
// Requires AdminService permission.
|
||||
// Uses a transaction to atomically clear the previous current flag and set the new one.
|
||||
//
|
||||
// Returns:
|
||||
// - 200 OK with activated EulaVersion on success
|
||||
// - 401 Unauthorized if not authenticated
|
||||
// - 403 Forbidden if not authorized
|
||||
// - 404 Not Found if version doesn't exist
|
||||
// - 409 Conflict if another admin activated a different version concurrently
|
||||
// - 500 Internal Server Error on database errors
|
||||
func (s *Controllers) ActivateEulaVersion(ctx echo.Context, versionID EulaVersionID) error {
|
||||
id, err := convertVersionIDParam(versionID)
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
|
||||
Message: "Invalid version ID format",
|
||||
})
|
||||
}
|
||||
|
||||
// Get admin user for audit
|
||||
adminUser, err := getAdminUserForAudit(ctx)
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
|
||||
Message: "Could not identify admin user",
|
||||
})
|
||||
}
|
||||
|
||||
version, err := s.svc.Eula.ActivateVersion(ctx.Request().Context(), id, adminUser.Email)
|
||||
if err != nil {
|
||||
if errors.Is(err, eula.ErrVersionNotFound) {
|
||||
return ctx.JSON(http.StatusNotFound, ErrorMessage{
|
||||
Message: "EULA version not found",
|
||||
})
|
||||
}
|
||||
if errors.Is(err, eula.ErrConcurrentActivation) {
|
||||
return ctx.JSON(http.StatusConflict, ErrorMessage{
|
||||
Message: "Concurrent activation conflict - another version was activated. Please refresh and try again.",
|
||||
})
|
||||
}
|
||||
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
||||
Message: "Failed to activate EULA version",
|
||||
})
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, convertVersionToAPI(version))
|
||||
}
|
||||
|
||||
// ListEulaAgreements returns a paginated list of EULA agreements.
|
||||
// GET /admin/eula/agreements
|
||||
//
|
||||
// Requires AdminService permission.
|
||||
//
|
||||
// Parameters:
|
||||
// - page: Page number (1-based, default: 1)
|
||||
// - page_size: Items per page (1-100, default: 20)
|
||||
// - version_id: Optional filter by EULA version
|
||||
// - user_id: Optional filter by Cognito subject ID
|
||||
//
|
||||
// Returns:
|
||||
// - 200 OK with EulaAgreementList on success
|
||||
// - 401 Unauthorized if not authenticated
|
||||
// - 403 Forbidden if not authorized
|
||||
// - 500 Internal Server Error on database errors
|
||||
func (s *Controllers) ListEulaAgreements(ctx echo.Context, params ListEulaAgreementsParams) error {
|
||||
// Apply defaults
|
||||
page := int32(1)
|
||||
pageSize := int32(20)
|
||||
if params.Page != nil {
|
||||
page = *params.Page
|
||||
}
|
||||
if params.PageSize != nil {
|
||||
pageSize = *params.PageSize
|
||||
}
|
||||
|
||||
input := &eula.ListAgreementsInput{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}
|
||||
|
||||
// Convert optional UUID filter
|
||||
if params.VersionId != nil {
|
||||
versionID := uuid.UUID(*params.VersionId)
|
||||
input.VersionID = &versionID
|
||||
}
|
||||
if params.UserId != nil {
|
||||
input.UserID = params.UserId
|
||||
}
|
||||
|
||||
result, err := s.svc.Eula.ListAgreements(ctx.Request().Context(), input)
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
||||
Message: "Failed to list EULA agreements",
|
||||
})
|
||||
}
|
||||
|
||||
// Convert to API response
|
||||
agreements := make([]EulaAgreement, len(result.Agreements))
|
||||
for i, a := range result.Agreements {
|
||||
agreements[i] = convertAgreementToAPI(a)
|
||||
}
|
||||
|
||||
response := EulaAgreementList{
|
||||
Agreements: agreements,
|
||||
Total: int32(result.Total), // #nosec G115 -- Total is bounded by pageSize pagination
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
HasMore: &result.HasMore,
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetEulaCompliance returns a comprehensive compliance report.
|
||||
// GET /admin/eula/compliance
|
||||
//
|
||||
// Requires AdminService permission.
|
||||
// This endpoint queries Cognito for all users and compares with EULA agreements.
|
||||
//
|
||||
// Parameters:
|
||||
// - page: Page number (1-based, default: 1)
|
||||
// - page_size: Items per page (1-100, default: 50)
|
||||
// - version_id: Optional specific version to report on (defaults to current)
|
||||
// - agreed: Optional filter by agreement status
|
||||
//
|
||||
// Returns:
|
||||
// - 200 OK with EulaComplianceReport on success
|
||||
// - 401 Unauthorized if not authenticated
|
||||
// - 403 Forbidden if not authorized
|
||||
// - 404 Not Found if specified version doesn't exist or no current EULA
|
||||
// - 502 Bad Gateway on Cognito errors
|
||||
// - 500 Internal Server Error on database errors
|
||||
func (s *Controllers) GetEulaCompliance(ctx echo.Context, params GetEulaComplianceParams) error {
|
||||
// Initialize AWS config for Cognito
|
||||
awsCfg, err := aws.GetAWSConfig(context.Background())
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
||||
Message: "Failed to initialize AWS configuration",
|
||||
})
|
||||
}
|
||||
|
||||
cognitoClient := cognitoidentityprovider.NewFromConfig(awsCfg)
|
||||
userPoolID := s.cfg.GetAuthUserPoolID()
|
||||
|
||||
// Build service input from params
|
||||
input := &eula.ComplianceReportInput{
|
||||
Page: 1,
|
||||
PageSize: 50,
|
||||
}
|
||||
|
||||
if params.Page != nil {
|
||||
input.Page = *params.Page
|
||||
}
|
||||
if params.PageSize != nil {
|
||||
input.PageSize = *params.PageSize
|
||||
}
|
||||
if params.VersionId != nil {
|
||||
// Convert openapi_types.UUID to uuid.UUID
|
||||
versionID := uuid.UUID(*params.VersionId)
|
||||
input.VersionID = &versionID
|
||||
}
|
||||
if params.Agreed != nil {
|
||||
input.Agreed = params.Agreed
|
||||
}
|
||||
|
||||
// Call service method
|
||||
result, err := s.svc.Eula.GetComplianceReport(ctx.Request().Context(), cognitoClient, userPoolID, input)
|
||||
if err != nil {
|
||||
if errors.Is(err, eula.ErrNoCurrentVersion) {
|
||||
return ctx.JSON(http.StatusNotFound, ErrorMessage{
|
||||
Message: "No current EULA version configured",
|
||||
})
|
||||
}
|
||||
if errors.Is(err, eula.ErrVersionNotFound) {
|
||||
return ctx.JSON(http.StatusNotFound, ErrorMessage{
|
||||
Message: "EULA version not found",
|
||||
})
|
||||
}
|
||||
// Check if it's a Cognito error
|
||||
if strings.Contains(err.Error(), "Cognito") || strings.Contains(err.Error(), "cognito") {
|
||||
return ctx.JSON(http.StatusBadGateway, ErrorMessage{
|
||||
Message: "Failed to fetch users from Cognito: " + err.Error(),
|
||||
})
|
||||
}
|
||||
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
||||
Message: "Failed to generate compliance report",
|
||||
})
|
||||
}
|
||||
|
||||
// Convert result to API response
|
||||
response := convertComplianceReportToAPI(result)
|
||||
|
||||
return ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// convertComplianceReportToAPI converts the service result to the API response type.
|
||||
func convertComplianceReportToAPI(result *eula.ComplianceReportResult) EulaComplianceReport {
|
||||
// Convert version to summary format
|
||||
versionSummary := EulaVersionSummary{
|
||||
Id: openapi_types.UUID(result.Version.ID),
|
||||
Version: result.Version.Version,
|
||||
Title: result.Version.Title,
|
||||
IsCurrent: result.Version.Iscurrent,
|
||||
}
|
||||
// Handle nullable effectiveDate
|
||||
if result.Version.Effectivedate.Valid {
|
||||
versionSummary.EffectiveDate = nullable.NewNullableWithValue(result.Version.Effectivedate.Time)
|
||||
}
|
||||
|
||||
// Convert summary
|
||||
summary := EulaComplianceSummary{
|
||||
TotalUsers: result.Summary.TotalUsers,
|
||||
AgreedCount: result.Summary.AgreedCount,
|
||||
NotAgreedCount: result.Summary.NotAgreedCount,
|
||||
CompliancePercentage: result.Summary.CompliancePercentage,
|
||||
}
|
||||
|
||||
// Convert users
|
||||
users := make([]EulaComplianceUser, 0, len(result.Users))
|
||||
for _, u := range result.Users {
|
||||
user := EulaComplianceUser{
|
||||
CognitoSubjectId: u.CognitoSubjectID,
|
||||
CurrentEmail: openapi_types.Email(u.CurrentEmail),
|
||||
Agreed: u.Agreed,
|
||||
}
|
||||
|
||||
if u.Email != nil {
|
||||
user.Email = nullable.NewNullableWithValue(openapi_types.Email(*u.Email))
|
||||
}
|
||||
if u.AgreedAt != nil {
|
||||
user.AgreedAt = nullable.NewNullableWithValue(*u.AgreedAt)
|
||||
}
|
||||
if u.AgreedFromIP != nil {
|
||||
user.AgreedFromIp = nullable.NewNullableWithValue(*u.AgreedFromIP)
|
||||
}
|
||||
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
// Build pagination
|
||||
pagination := EulaCompliancePagination{
|
||||
Page: result.Page,
|
||||
PageSize: result.PageSize,
|
||||
TotalPages: result.TotalPages,
|
||||
TotalItems: result.TotalItems,
|
||||
}
|
||||
|
||||
return EulaComplianceReport{
|
||||
EulaVersion: versionSummary,
|
||||
Summary: summary,
|
||||
Users: users,
|
||||
Pagination: pagination,
|
||||
}
|
||||
}
|
||||
|
||||
// convertVersionToAPI converts a repository Eulaversion to the API EulaVersion type.
|
||||
func convertVersionToAPI(v *repository.Eulaversion) EulaVersion {
|
||||
version := EulaVersion{
|
||||
Id: openapi_types.UUID(v.ID),
|
||||
Version: v.Version,
|
||||
Title: v.Title,
|
||||
Content: v.Content,
|
||||
CreatedAt: v.Createdat.Time,
|
||||
CreatedBy: openapi_types.Email(v.Createdby),
|
||||
IsCurrent: v.Iscurrent,
|
||||
}
|
||||
|
||||
// Handle nullable effectiveDate
|
||||
if v.Effectivedate.Valid {
|
||||
version.EffectiveDate = nullable.NewNullableWithValue(v.Effectivedate.Time)
|
||||
}
|
||||
|
||||
if v.Activatedat.Valid {
|
||||
version.ActivatedAt = nullable.NewNullableWithValue(v.Activatedat.Time)
|
||||
}
|
||||
if v.Activatedby != nil {
|
||||
version.ActivatedBy = nullable.NewNullableWithValue(openapi_types.Email(*v.Activatedby))
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
// convertAgreementToAPI converts a repository ListEulaAgreementsRow to the API EulaAgreement type.
|
||||
func convertAgreementToAPI(a *repository.ListEulaAgreementsRow) EulaAgreement {
|
||||
var agreedAt time.Time
|
||||
if a.Agreedat.Valid {
|
||||
agreedAt = a.Agreedat.Time
|
||||
}
|
||||
|
||||
return EulaAgreement{
|
||||
Id: openapi_types.UUID(a.ID),
|
||||
CognitoSubjectId: a.Cognitosubjectid,
|
||||
UserEmail: openapi_types.Email(a.Useremail),
|
||||
EulaVersionId: openapi_types.UUID(a.Eulaversionid),
|
||||
EulaVersion: a.Eulaversionstring,
|
||||
AgreedAt: agreedAt,
|
||||
AgreedFromIp: a.Agreedfromip,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,609 @@
|
||||
package queryapi_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
queryapi "queryorchestration/api/queryAPI"
|
||||
"queryorchestration/internal/cognitoauth"
|
||||
"queryorchestration/internal/eula"
|
||||
"queryorchestration/internal/test"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/oapi-codegen/nullable"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ptrTime is a helper to create a pointer to a time.Time value.
|
||||
func ptrTime(t time.Time) *time.Time {
|
||||
return &t
|
||||
}
|
||||
|
||||
// setupEulaTestContext creates an echo context with the specified path and method.
|
||||
func setupEulaTestContext(method, path string, body interface{}) (echo.Context, *httptest.ResponseRecorder) {
|
||||
e := echo.New()
|
||||
var req *http.Request
|
||||
|
||||
if body != nil {
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
req = httptest.NewRequest(method, path, bytes.NewReader(jsonBody))
|
||||
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
||||
} else {
|
||||
req = httptest.NewRequest(method, path, nil)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
ctx := e.NewContext(req, rec)
|
||||
return ctx, rec
|
||||
}
|
||||
|
||||
// setupEulaTestContextWithUser creates an echo context with user info set.
|
||||
func setupEulaTestContextWithUser(method, path string, body interface{}, userSubject, userEmail string) (echo.Context, *httptest.ResponseRecorder) {
|
||||
ctx, rec := setupEulaTestContext(method, path, body)
|
||||
|
||||
// Set up user info in context (simulating authenticated user)
|
||||
// Note: GetUserSubject expects map[string]interface{} with "sub" key
|
||||
// Note: GetUserInfo expects cognitoauth.UserInfo (not pointer)
|
||||
ctx.Set("user_claims", map[string]interface{}{
|
||||
"sub": userSubject,
|
||||
"email": userEmail,
|
||||
})
|
||||
ctx.Set("user_info", cognitoauth.UserInfo{
|
||||
Email: userEmail,
|
||||
})
|
||||
|
||||
return ctx, rec
|
||||
}
|
||||
|
||||
// createTestEulaVersion creates a test EULA version in the database.
|
||||
func createTestEulaVersion(t *testing.T, svc *eula.Service, version, title string, activate bool) *uuid.UUID {
|
||||
ctx := t.Context()
|
||||
|
||||
input := &eula.CreateVersionInput{
|
||||
Version: version,
|
||||
Title: title,
|
||||
Content: "# Test EULA\n\nThis is test content.",
|
||||
EffectiveDate: ptrTime(time.Now()),
|
||||
CreatedBy: "admin@test.com",
|
||||
}
|
||||
|
||||
created, err := svc.CreateVersion(ctx, input)
|
||||
require.NoError(t, err)
|
||||
|
||||
if activate {
|
||||
_, err = svc.ActivateVersion(ctx, created.ID, "admin@test.com")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return &created.ID
|
||||
}
|
||||
|
||||
func TestGetCurrentEula(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
t.Run("returns current EULA version", func(t *testing.T) {
|
||||
// Create and activate a version
|
||||
versionStr := "1.0.0-test-" + uuid.New().String()[:8]
|
||||
createTestEulaVersion(t, svc.Eula, versionStr, "Test EULA", true)
|
||||
|
||||
ctx, rec := setupEulaTestContext(http.MethodGet, "/eula", nil)
|
||||
|
||||
err := cons.GetCurrentEula(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response queryapi.EulaPublicResponse
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, versionStr, response.Version)
|
||||
})
|
||||
|
||||
t.Run("returns 404 when no current version", func(t *testing.T) {
|
||||
// Create a new config without any EULA versions activated
|
||||
cfg2 := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg2)
|
||||
initializeTestConfig(t, cfg2)
|
||||
|
||||
svc2 := createControllerServices(cfg2)
|
||||
cons2 := queryapi.NewControllers(svc2, cfg2)
|
||||
|
||||
ctx, rec := setupEulaTestContext(http.MethodGet, "/eula", nil)
|
||||
|
||||
err := cons2.GetCurrentEula(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetEulaStatus(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
// Create and activate a version
|
||||
versionStr := "status-test-" + uuid.New().String()[:8]
|
||||
versionID := createTestEulaVersion(t, svc.Eula, versionStr, "Status Test EULA", true)
|
||||
|
||||
t.Run("returns status for unagreed user", func(t *testing.T) {
|
||||
userSubject := "user-" + uuid.New().String()[:8]
|
||||
ctx, rec := setupEulaTestContextWithUser(http.MethodGet, "/eula/status", nil, userSubject, "user@test.com")
|
||||
|
||||
err := cons.GetEulaStatus(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response queryapi.EulaStatusResponse
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, response.HasAgreed)
|
||||
assert.Equal(t, versionStr, response.CurrentVersion)
|
||||
})
|
||||
|
||||
t.Run("returns status for agreed user", func(t *testing.T) {
|
||||
userSubject := "agreed-user-" + uuid.New().String()[:8]
|
||||
userEmail := "agreed@test.com"
|
||||
|
||||
// Record agreement
|
||||
_, err := svc.Eula.RecordAgreement(t.Context(), &eula.RecordAgreementInput{
|
||||
CognitoSubjectID: userSubject,
|
||||
UserEmail: userEmail,
|
||||
EulaVersionID: *versionID,
|
||||
IPAddress: "192.168.1.1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, rec := setupEulaTestContextWithUser(http.MethodGet, "/eula/status", nil, userSubject, userEmail)
|
||||
|
||||
err = cons.GetEulaStatus(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response queryapi.EulaStatusResponse
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, response.HasAgreed)
|
||||
})
|
||||
|
||||
t.Run("returns 401 for unauthenticated user", func(t *testing.T) {
|
||||
ctx, rec := setupEulaTestContext(http.MethodGet, "/eula/status", nil)
|
||||
// Don't set user info
|
||||
|
||||
err := cons.GetEulaStatus(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusUnauthorized, rec.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAgreeToEula(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
// Create and activate a version
|
||||
versionStr := "agree-test-" + uuid.New().String()[:8]
|
||||
createTestEulaVersion(t, svc.Eula, versionStr, "Agree Test EULA", true)
|
||||
|
||||
t.Run("records new agreement", func(t *testing.T) {
|
||||
userSubject := "agree-user-" + uuid.New().String()[:8]
|
||||
userEmail := "agreeuser@test.com"
|
||||
ctx, rec := setupEulaTestContextWithUser(http.MethodPost, "/eula/agree", nil, userSubject, userEmail)
|
||||
ctx.Request().RemoteAddr = "192.168.1.100:12345"
|
||||
|
||||
err := cons.AgreeToEula(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusCreated, rec.Code)
|
||||
|
||||
var response queryapi.EulaAgreementResponse
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, versionStr, response.EulaVersion)
|
||||
})
|
||||
|
||||
t.Run("idempotent - returns 200 for existing agreement", func(t *testing.T) {
|
||||
userSubject := "idempotent-user-" + uuid.New().String()[:8]
|
||||
userEmail := "idempotent@test.com"
|
||||
|
||||
// First call
|
||||
ctx1, rec1 := setupEulaTestContextWithUser(http.MethodPost, "/eula/agree", nil, userSubject, userEmail)
|
||||
ctx1.Request().RemoteAddr = "192.168.1.100:12345"
|
||||
err := cons.AgreeToEula(ctx1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusCreated, rec1.Code)
|
||||
|
||||
// Second call (same user)
|
||||
ctx2, rec2 := setupEulaTestContextWithUser(http.MethodPost, "/eula/agree", nil, userSubject, userEmail)
|
||||
ctx2.Request().RemoteAddr = "192.168.1.100:12345"
|
||||
err = cons.AgreeToEula(ctx2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec2.Code)
|
||||
})
|
||||
|
||||
t.Run("returns 401 for unauthenticated user", func(t *testing.T) {
|
||||
ctx, rec := setupEulaTestContext(http.MethodPost, "/eula/agree", nil)
|
||||
|
||||
err := cons.AgreeToEula(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusUnauthorized, rec.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListEulaVersions(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
// Create multiple versions
|
||||
for i := 0; i < 5; i++ {
|
||||
createTestEulaVersion(t, svc.Eula, "list-v"+uuid.New().String()[:8], "List EULA", false)
|
||||
}
|
||||
|
||||
t.Run("lists all versions with pagination", func(t *testing.T) {
|
||||
ctx, rec := setupEulaTestContext(http.MethodGet, "/admin/eula?page=1&page_size=3", nil)
|
||||
|
||||
page := int32(1)
|
||||
pageSize := int32(3)
|
||||
params := queryapi.ListEulaVersionsParams{
|
||||
Page: &page,
|
||||
PageSize: &pageSize,
|
||||
}
|
||||
|
||||
err := cons.ListEulaVersions(ctx, params)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response queryapi.EulaVersionList
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.LessOrEqual(t, len(response.Versions), 3)
|
||||
assert.GreaterOrEqual(t, response.Total, int32(5))
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateEulaVersion(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
t.Run("creates new EULA version", func(t *testing.T) {
|
||||
versionStr := "create-test-" + uuid.New().String()[:8]
|
||||
body := queryapi.EulaVersionCreate{
|
||||
Version: versionStr,
|
||||
Title: "Created EULA",
|
||||
Content: "# Created EULA Content\n\nTest content here.",
|
||||
EffectiveDate: nullable.NewNullableWithValue(time.Now().Add(24 * time.Hour)),
|
||||
}
|
||||
|
||||
ctx, rec := setupEulaTestContextWithUser(http.MethodPost, "/admin/eula", body, "admin-subject", "admin@test.com")
|
||||
|
||||
err := cons.CreateEulaVersion(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusCreated, rec.Code)
|
||||
|
||||
var response queryapi.EulaVersion
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, versionStr, response.Version)
|
||||
assert.Equal(t, "Created EULA", response.Title)
|
||||
})
|
||||
|
||||
t.Run("returns 409 for duplicate version", func(t *testing.T) {
|
||||
versionStr := "dup-test-" + uuid.New().String()[:8]
|
||||
body := queryapi.EulaVersionCreate{
|
||||
Version: versionStr,
|
||||
Title: "First EULA",
|
||||
Content: "Content",
|
||||
EffectiveDate: nullable.NewNullableWithValue(time.Now()),
|
||||
}
|
||||
|
||||
// First creation
|
||||
ctx1, _ := setupEulaTestContextWithUser(http.MethodPost, "/admin/eula", body, "admin-subject", "admin@test.com")
|
||||
err := cons.CreateEulaVersion(ctx1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Second creation (same version)
|
||||
ctx2, rec2 := setupEulaTestContextWithUser(http.MethodPost, "/admin/eula", body, "admin-subject", "admin@test.com")
|
||||
err = cons.CreateEulaVersion(ctx2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusConflict, rec2.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetEulaVersion(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
t.Run("gets existing version by ID", func(t *testing.T) {
|
||||
versionStr := "get-test-" + uuid.New().String()[:8]
|
||||
versionID := createTestEulaVersion(t, svc.Eula, versionStr, "Get Test EULA", false)
|
||||
|
||||
ctx, rec := setupEulaTestContext(http.MethodGet, "/admin/eula/"+versionID.String(), nil)
|
||||
|
||||
err := cons.GetEulaVersion(ctx, *versionID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response queryapi.EulaVersion
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, versionStr, response.Version)
|
||||
})
|
||||
|
||||
t.Run("returns 404 for non-existent version", func(t *testing.T) {
|
||||
nonExistentID := uuid.New()
|
||||
ctx, rec := setupEulaTestContext(http.MethodGet, "/admin/eula/"+nonExistentID.String(), nil)
|
||||
|
||||
err := cons.GetEulaVersion(ctx, nonExistentID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateEulaVersion(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
t.Run("updates version title", func(t *testing.T) {
|
||||
versionStr := "update-test-" + uuid.New().String()[:8]
|
||||
versionID := createTestEulaVersion(t, svc.Eula, versionStr, "Original Title", false)
|
||||
|
||||
newTitle := "Updated Title"
|
||||
body := queryapi.EulaVersionUpdate{
|
||||
Title: &newTitle,
|
||||
}
|
||||
|
||||
ctx, rec := setupEulaTestContext(http.MethodPatch, "/admin/eula/"+versionID.String(), body)
|
||||
|
||||
err := cons.UpdateEulaVersion(ctx, *versionID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response queryapi.EulaVersion
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newTitle, response.Title)
|
||||
})
|
||||
}
|
||||
|
||||
func TestActivateEulaVersion(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
t.Run("activates version", func(t *testing.T) {
|
||||
versionStr := "activate-test-" + uuid.New().String()[:8]
|
||||
versionID := createTestEulaVersion(t, svc.Eula, versionStr, "Activate Test EULA", false)
|
||||
|
||||
ctx, rec := setupEulaTestContextWithUser(http.MethodPost, "/admin/eula/"+versionID.String()+"/activate", nil, "admin-subject", "admin@test.com")
|
||||
|
||||
err := cons.ActivateEulaVersion(ctx, *versionID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response queryapi.EulaVersion
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, response.IsCurrent)
|
||||
})
|
||||
|
||||
t.Run("returns 404 for non-existent version", func(t *testing.T) {
|
||||
nonExistentID := uuid.New()
|
||||
ctx, rec := setupEulaTestContextWithUser(http.MethodPost, "/admin/eula/"+nonExistentID.String()+"/activate", nil, "admin-subject", "admin@test.com")
|
||||
|
||||
err := cons.ActivateEulaVersion(ctx, nonExistentID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListEulaAgreements(t *testing.T) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
// Create version and some agreements
|
||||
versionID := createTestEulaVersion(t, svc.Eula, "agreements-test-"+uuid.New().String()[:8], "Agreements Test", true)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
_, err := svc.Eula.RecordAgreement(t.Context(), &eula.RecordAgreementInput{
|
||||
CognitoSubjectID: "user-" + uuid.New().String()[:8],
|
||||
UserEmail: "user" + uuid.New().String()[:4] + "@test.com",
|
||||
EulaVersionID: *versionID,
|
||||
IPAddress: "192.168.1.1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("lists agreements", func(t *testing.T) {
|
||||
ctx, rec := setupEulaTestContext(http.MethodGet, "/admin/eula/agreements", nil)
|
||||
|
||||
page := int32(1)
|
||||
pageSize := int32(10)
|
||||
params := queryapi.ListEulaAgreementsParams{
|
||||
Page: &page,
|
||||
PageSize: &pageSize,
|
||||
}
|
||||
|
||||
err := cons.ListEulaAgreements(ctx, params)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response queryapi.EulaAgreementList
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(response.Agreements), 3)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetEulaCompliance(t *testing.T) {
|
||||
// This test requires real Cognito access - opt-in via env var
|
||||
if os.Getenv("ENABLE_COGNITO_INTEGRATION_TESTS") != "true" {
|
||||
t.Skip("Skipping Cognito integration test: set ENABLE_COGNITO_INTEGRATION_TESTS=true to run")
|
||||
}
|
||||
|
||||
userPoolID := os.Getenv("COGNITO_USER_POOL_ID")
|
||||
awsRegion := os.Getenv("AWS_REGION")
|
||||
if awsRegion == "" {
|
||||
awsRegion = os.Getenv("AUTH_REGION")
|
||||
}
|
||||
|
||||
if userPoolID == "" || awsRegion == "" {
|
||||
t.Skip("Skipping compliance handler test: COGNITO_USER_POOL_ID and AWS_REGION/AUTH_REGION must be set")
|
||||
}
|
||||
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
// Create and activate a version for testing
|
||||
versionStr := "compliance-handler-" + uuid.New().String()[:8]
|
||||
createTestEulaVersion(t, svc.Eula, versionStr, "Compliance Handler Test EULA", true)
|
||||
|
||||
t.Run("returns_compliance_report", func(t *testing.T) {
|
||||
ctx, rec := setupEulaTestContext(http.MethodGet, "/admin/eula/compliance", nil)
|
||||
|
||||
params := queryapi.GetEulaComplianceParams{}
|
||||
err := cons.GetEulaCompliance(ctx, params)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response queryapi.EulaComplianceReport
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify structure
|
||||
assert.NotEmpty(t, response.EulaVersion.Id)
|
||||
assert.NotEmpty(t, response.EulaVersion.Version)
|
||||
assert.GreaterOrEqual(t, response.Summary.TotalUsers, int32(0))
|
||||
assert.Equal(t, response.Summary.TotalUsers, response.Summary.AgreedCount+response.Summary.NotAgreedCount)
|
||||
})
|
||||
|
||||
t.Run("returns_404_for_nonexistent_version", func(t *testing.T) {
|
||||
ctx, rec := setupEulaTestContext(http.MethodGet, "/admin/eula/compliance?version_id=00000000-0000-0000-0000-000000000000", nil)
|
||||
|
||||
nonExistentID := uuid.MustParse("00000000-0000-0000-0000-000000000000")
|
||||
params := queryapi.GetEulaComplianceParams{
|
||||
VersionId: (*queryapi.EulaVersionID)(&nonExistentID),
|
||||
}
|
||||
err := cons.GetEulaCompliance(ctx, params)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code)
|
||||
})
|
||||
|
||||
t.Run("filters_by_agreed_status", func(t *testing.T) {
|
||||
ctx, rec := setupEulaTestContext(http.MethodGet, "/admin/eula/compliance?agreed=false", nil)
|
||||
|
||||
agreed := false
|
||||
params := queryapi.GetEulaComplianceParams{
|
||||
Agreed: &agreed,
|
||||
}
|
||||
err := cons.GetEulaCompliance(ctx, params)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response queryapi.EulaComplianceReport
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
// All users in the result should have agreed=false
|
||||
for _, user := range response.Users {
|
||||
assert.False(t, user.Agreed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pagination_params_work", func(t *testing.T) {
|
||||
ctx, rec := setupEulaTestContext(http.MethodGet, "/admin/eula/compliance?page=1&page_size=5", nil)
|
||||
|
||||
page := int32(1)
|
||||
pageSize := int32(5)
|
||||
params := queryapi.GetEulaComplianceParams{
|
||||
Page: &page,
|
||||
PageSize: &pageSize,
|
||||
}
|
||||
err := cons.GetEulaCompliance(ctx, params)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var response queryapi.EulaComplianceReport
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, int32(1), response.Pagination.Page)
|
||||
assert.Equal(t, int32(5), response.Pagination.PageSize)
|
||||
assert.LessOrEqual(t, len(response.Users), 5)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetEulaComplianceNoCurrentVersion(t *testing.T) {
|
||||
// This test requires real Cognito access - opt-in via env var
|
||||
if os.Getenv("ENABLE_COGNITO_INTEGRATION_TESTS") != "true" {
|
||||
t.Skip("Skipping Cognito integration test: set ENABLE_COGNITO_INTEGRATION_TESTS=true to run")
|
||||
}
|
||||
|
||||
userPoolID := os.Getenv("COGNITO_USER_POOL_ID")
|
||||
awsRegion := os.Getenv("AWS_REGION")
|
||||
if awsRegion == "" {
|
||||
awsRegion = os.Getenv("AUTH_REGION")
|
||||
}
|
||||
|
||||
if userPoolID == "" || awsRegion == "" {
|
||||
t.Skip("Skipping compliance handler test: COGNITO_USER_POOL_ID and AWS_REGION/AUTH_REGION must be set")
|
||||
}
|
||||
|
||||
// Fresh database with no EULA versions
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
t.Run("returns_404_no_current_eula", func(t *testing.T) {
|
||||
ctx, rec := setupEulaTestContext(http.MethodGet, "/admin/eula/compliance", nil)
|
||||
|
||||
params := queryapi.GetEulaComplianceParams{}
|
||||
err := cons.GetEulaCompliance(ctx, params)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// eulaPublicHandlers.go implements the public EULA API handlers.
|
||||
// These endpoints handle EULA retrieval and user agreement recording.
|
||||
package queryapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"queryorchestration/internal/cognitoauth"
|
||||
"queryorchestration/internal/eula"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/oapi-codegen/nullable"
|
||||
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||
)
|
||||
|
||||
// GetCurrentEula returns the current active EULA version.
|
||||
// This endpoint is public and does not require authentication.
|
||||
// GET /eula
|
||||
//
|
||||
// Returns:
|
||||
// - 200 OK with EulaPublicResponse on success
|
||||
// - 404 Not Found if no EULA is currently active
|
||||
// - 500 Internal Server Error on database errors
|
||||
func (s *Controllers) GetCurrentEula(ctx echo.Context) error {
|
||||
version, err := s.svc.Eula.GetCurrentVersion(ctx.Request().Context())
|
||||
if err != nil {
|
||||
if errors.Is(err, eula.ErrNoCurrentVersion) {
|
||||
return ctx.JSON(http.StatusNotFound, ErrorMessage{
|
||||
Message: "No current EULA version configured",
|
||||
})
|
||||
}
|
||||
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
||||
Message: "Failed to get current EULA version",
|
||||
})
|
||||
}
|
||||
|
||||
// Convert to public response
|
||||
response := EulaPublicResponse{
|
||||
Id: openapi_types.UUID(version.ID),
|
||||
Version: version.Version,
|
||||
Title: version.Title,
|
||||
Content: version.Content,
|
||||
}
|
||||
// Handle nullable effectiveDate
|
||||
if version.Effectivedate.Valid {
|
||||
response.EffectiveDate = nullable.NewNullableWithValue(version.Effectivedate.Time)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetEulaStatus returns the current user's agreement status for the current EULA.
|
||||
// Used by the frontend to determine if the user needs to be prompted to agree.
|
||||
// GET /eula/status
|
||||
//
|
||||
// Requires authentication via JWT.
|
||||
//
|
||||
// Returns:
|
||||
// - 200 OK with EulaStatusResponse on success
|
||||
// - 401 Unauthorized if not authenticated
|
||||
// - 404 Not Found if no EULA is currently active
|
||||
// - 500 Internal Server Error on database errors
|
||||
func (s *Controllers) GetEulaStatus(ctx echo.Context) error {
|
||||
// Get user subject from JWT
|
||||
userSubject, ok := cognitoauth.GetUserSubject(ctx)
|
||||
if !ok {
|
||||
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
|
||||
Message: "Authentication required",
|
||||
})
|
||||
}
|
||||
|
||||
status, err := s.svc.Eula.GetUserEulaStatus(ctx.Request().Context(), userSubject)
|
||||
if err != nil {
|
||||
if errors.Is(err, eula.ErrNoCurrentVersion) {
|
||||
return ctx.JSON(http.StatusNotFound, ErrorMessage{
|
||||
Message: "No current EULA version configured",
|
||||
})
|
||||
}
|
||||
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
||||
Message: "Failed to get EULA status",
|
||||
})
|
||||
}
|
||||
|
||||
// Convert to API response
|
||||
response := EulaStatusResponse{
|
||||
HasAgreed: status.HasAgreed,
|
||||
CurrentVersion: status.CurrentVersion,
|
||||
CurrentVersionId: openapi_types.UUID(status.CurrentVersionID),
|
||||
}
|
||||
|
||||
if status.AgreedAt != nil {
|
||||
response.AgreedAt = nullable.NewNullableWithValue(*status.AgreedAt)
|
||||
}
|
||||
if status.AgreedVersion != nil {
|
||||
response.AgreedVersion = nullable.NewNullableWithValue(*status.AgreedVersion)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// AgreeToEula records the current user's agreement to the current EULA version.
|
||||
// This operation is idempotent - calling it multiple times will not create duplicate records.
|
||||
// POST /eula/agree
|
||||
//
|
||||
// Requires authentication via JWT.
|
||||
// IP address is automatically captured from the request.
|
||||
//
|
||||
// Returns:
|
||||
// - 201 Created with EulaAgreementResponse for new agreements
|
||||
// - 200 OK with EulaAgreementResponse if user already agreed (idempotent)
|
||||
// - 400 Bad Request if no current EULA is configured
|
||||
// - 401 Unauthorized if not authenticated
|
||||
// - 500 Internal Server Error on database errors
|
||||
func (s *Controllers) AgreeToEula(ctx echo.Context) error {
|
||||
// Get user info from JWT
|
||||
userSubject, subjectOK := cognitoauth.GetUserSubject(ctx)
|
||||
userInfo, infoOK := cognitoauth.GetUserInfo(ctx)
|
||||
if !subjectOK || !infoOK {
|
||||
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
|
||||
Message: "Authentication required",
|
||||
})
|
||||
}
|
||||
|
||||
// Get current EULA version
|
||||
currentVersion, err := s.svc.Eula.GetCurrentVersion(ctx.Request().Context())
|
||||
if err != nil {
|
||||
if errors.Is(err, eula.ErrNoCurrentVersion) {
|
||||
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
|
||||
Message: "No current EULA version configured",
|
||||
})
|
||||
}
|
||||
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
||||
Message: "Failed to get current EULA version",
|
||||
})
|
||||
}
|
||||
|
||||
// Get client IP address
|
||||
ipAddress := ctx.RealIP()
|
||||
if ipAddress == "" {
|
||||
ipAddress = ctx.Request().RemoteAddr
|
||||
}
|
||||
|
||||
// Record the agreement
|
||||
result, err := s.svc.Eula.RecordAgreement(ctx.Request().Context(), &eula.RecordAgreementInput{
|
||||
CognitoSubjectID: userSubject,
|
||||
UserEmail: userInfo.Email,
|
||||
EulaVersionID: currentVersion.ID,
|
||||
IPAddress: ipAddress,
|
||||
})
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
||||
Message: "Failed to record EULA agreement",
|
||||
})
|
||||
}
|
||||
|
||||
// Build response
|
||||
var agreedAt time.Time
|
||||
if result.Agreement.Agreedat.Valid {
|
||||
agreedAt = result.Agreement.Agreedat.Time
|
||||
}
|
||||
|
||||
response := EulaAgreementResponse{
|
||||
Id: openapi_types.UUID(result.Agreement.ID),
|
||||
EulaVersionId: openapi_types.UUID(result.Agreement.Eulaversionid),
|
||||
EulaVersion: currentVersion.Version,
|
||||
AgreedAt: agreedAt,
|
||||
}
|
||||
|
||||
// Return 200 if already agreed (idempotent), 201 for new agreement
|
||||
if result.AlreadyAgreed {
|
||||
return ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
return ctx.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// convertVersionIDParam converts the path parameter to uuid.UUID.
|
||||
// Returns error if the ID format is invalid.
|
||||
func convertVersionIDParam(versionID EulaVersionID) (uuid.UUID, error) {
|
||||
return uuid.UUID(versionID), nil
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"queryorchestration/internal/document"
|
||||
documentbatch "queryorchestration/internal/document/batch"
|
||||
documentupload "queryorchestration/internal/document/upload"
|
||||
"queryorchestration/internal/eula"
|
||||
"queryorchestration/internal/export"
|
||||
"queryorchestration/internal/fieldextraction"
|
||||
"queryorchestration/internal/folder"
|
||||
@@ -49,6 +50,7 @@ func createControllerServices(cfg *ControllerConfig) *queryapi.Services {
|
||||
fieldext := fieldextraction.New(cfg)
|
||||
fld := folder.New(cfg)
|
||||
lbl := label.New(cfg)
|
||||
eul := eula.New(cfg)
|
||||
|
||||
return &queryapi.Services{
|
||||
Export: exp,
|
||||
@@ -62,5 +64,6 @@ func createControllerServices(cfg *ControllerConfig) *queryapi.Services {
|
||||
FieldExtraction: fieldext,
|
||||
Folder: fld,
|
||||
Label: lbl,
|
||||
Eula: eul,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user