mirror of
https://github.com/PrivateCaptcha/PrivateCaptcha.git
synced 2026-02-08 23:09:11 -06:00
Improve test coverage for puzzle, portal, monitoring packages (#264)
* Initial plan * Add tests for puzzle, monitoring, portal packages Co-authored-by: ribtoks <505555+ribtoks@users.noreply.github.com> * Add registration disabled tests Co-authored-by: ribtoks <505555+ribtoks@users.noreply.github.com> * Address PR review comments: fix tests and use constants Co-authored-by: ribtoks <505555+ribtoks@users.noreply.github.com> * Fix audit tests with non-empty arrays and use constants for error strings Co-authored-by: ribtoks <505555+ribtoks@users.noreply.github.com> * Update audit.go * Update audit.go --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ribtoks <505555+ribtoks@users.noreply.github.com>
This commit is contained in:
@@ -32,7 +32,7 @@ func (al *AuditLog) Start(ctx context.Context, interval time.Duration) {
|
||||
var cancelCtx context.Context
|
||||
cancelCtx, al.persistCancel = context.WithCancel(
|
||||
context.WithValue(ctx, common.TraceIDContextKey, "persist_auditlog"))
|
||||
go common.ProcessBatchArray(cancelCtx, al.persistChan, interval, al.batchSize, al.batchSize*10, al.persistAuditLog)
|
||||
go common.ProcessBatchArray(cancelCtx, al.persistChan, interval, al.batchSize, al.batchSize*10, al.PersistAuditLog)
|
||||
}
|
||||
|
||||
func (al *AuditLog) Shutdown() {
|
||||
@@ -41,7 +41,7 @@ func (al *AuditLog) Shutdown() {
|
||||
close(al.persistChan)
|
||||
}
|
||||
|
||||
func (al *AuditLog) persistAuditLog(ctx context.Context, batch []*common.AuditLogEvent) error {
|
||||
func (al *AuditLog) PersistAuditLog(ctx context.Context, batch []*common.AuditLogEvent) error {
|
||||
dbBatch := make([]*dbgen.CreateAuditLogsParams, 0, len(batch))
|
||||
|
||||
for _, e := range batch {
|
||||
|
||||
@@ -570,3 +570,21 @@ func TestGetMacAddress(t *testing.T) {
|
||||
t.Error("Expected non-empty MAC address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCheckLicenseJobEmptyURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Save and restore LicenseURL
|
||||
originalURL := LicenseURL
|
||||
LicenseURL = ""
|
||||
defer func() { LicenseURL = originalURL }()
|
||||
|
||||
quitFunc := func(ctx context.Context) {}
|
||||
|
||||
_, err := NewCheckLicenseJob(nil, nil, "test-version", quitFunc)
|
||||
// We expect an error since activation keys are not available in test environment
|
||||
// or because LicenseURL is empty
|
||||
if err == nil {
|
||||
t.Error("Expected error from NewCheckLicenseJob with missing keys or empty URL")
|
||||
}
|
||||
}
|
||||
|
||||
262
pkg/monitoring/service_test.go
Normal file
262
pkg/monitoring/service_test.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/common"
|
||||
)
|
||||
|
||||
func TestNewService(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service := NewService()
|
||||
|
||||
if service == nil {
|
||||
t.Fatal("Expected NewService to return non-nil service")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceObservePuzzleCreated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service := NewService()
|
||||
|
||||
// Test that ObservePuzzleCreated does not panic
|
||||
service.ObservePuzzleCreated(123)
|
||||
service.ObservePuzzleCreated(456)
|
||||
}
|
||||
|
||||
func TestServiceObservePuzzleVerified(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service := NewService()
|
||||
|
||||
// Test various combinations of parameters
|
||||
service.ObservePuzzleVerified(123, "success", false)
|
||||
service.ObservePuzzleVerified(123, "failure", true)
|
||||
service.ObservePuzzleVerified(456, "error", false)
|
||||
}
|
||||
|
||||
func TestServiceObserveHttpError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service := NewService()
|
||||
|
||||
// Test that ObserveHttpError does not panic
|
||||
service.ObserveHttpError("/login", "GET", 400)
|
||||
service.ObserveHttpError("/register", "POST", 500)
|
||||
service.ObserveHttpError("/settings", "PUT", 404)
|
||||
}
|
||||
|
||||
func TestServiceObserveApiError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service := NewService()
|
||||
|
||||
// Test that ObserveApiError does not panic
|
||||
service.ObserveApiError("/api/puzzle", "GET", 401)
|
||||
service.ObserveApiError("/api/verify", "POST", 400)
|
||||
}
|
||||
|
||||
func TestServiceObserveCacheHitRatio(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service := NewService()
|
||||
|
||||
// Test various hit ratios
|
||||
service.ObserveCacheHitRatio(0.0)
|
||||
service.ObserveCacheHitRatio(0.5)
|
||||
service.ObserveCacheHitRatio(1.0)
|
||||
}
|
||||
|
||||
func TestServiceObserveEventDropped(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service := NewService()
|
||||
|
||||
// Test different event types
|
||||
service.ObserveEventDropped(common.PuzzleEventType)
|
||||
service.ObserveEventDropped(common.VerifyEventType)
|
||||
service.ObserveEventDropped(common.SessionEventType)
|
||||
}
|
||||
|
||||
func TestServiceObserveHealth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service := NewService()
|
||||
|
||||
// Test different health combinations
|
||||
service.ObserveHealth(true, true)
|
||||
service.ObserveHealth(true, false)
|
||||
service.ObserveHealth(false, true)
|
||||
service.ObserveHealth(false, false)
|
||||
}
|
||||
|
||||
func TestServiceHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service := NewService()
|
||||
|
||||
// Create a simple handler
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Wrap with metrics
|
||||
wrapped := service.Handler(inner)
|
||||
if wrapped == nil {
|
||||
t.Fatal("Expected Handler to return non-nil handler")
|
||||
}
|
||||
|
||||
// Test that the wrapped handler works
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceCDNHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service := NewService()
|
||||
|
||||
// Create a simple handler
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Wrap with CDN metrics
|
||||
wrapped := service.CDNHandler(inner)
|
||||
if wrapped == nil {
|
||||
t.Fatal("Expected CDNHandler to return non-nil handler")
|
||||
}
|
||||
|
||||
// Test that the wrapped handler works
|
||||
req := httptest.NewRequest("GET", "/cdn/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceIgnoredHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service := NewService()
|
||||
|
||||
// Create a simple handler
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Wrap with ignored metrics
|
||||
wrapped := service.IgnoredHandler(inner)
|
||||
if wrapped == nil {
|
||||
t.Fatal("Expected IgnoredHandler to return non-nil handler")
|
||||
}
|
||||
|
||||
// Test that the wrapped handler works
|
||||
req := httptest.NewRequest("GET", "/ignored", nil)
|
||||
w := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceHandlerIDFunc(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service := NewService()
|
||||
|
||||
handlerIDFunc := func() string {
|
||||
return "/custom/handler"
|
||||
}
|
||||
|
||||
middleware := service.HandlerIDFunc(handlerIDFunc)
|
||||
if middleware == nil {
|
||||
t.Fatal("Expected HandlerIDFunc to return non-nil middleware")
|
||||
}
|
||||
|
||||
// Create a simple handler
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Wrap with middleware
|
||||
wrapped := middleware(inner)
|
||||
if wrapped == nil {
|
||||
t.Fatal("Expected middleware to return non-nil handler")
|
||||
}
|
||||
|
||||
// Test that the wrapped handler works
|
||||
req := httptest.NewRequest("GET", "/custom/handler", nil)
|
||||
w := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogged(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a simple handler
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Wrap with logging
|
||||
wrapped := Logged(inner)
|
||||
if wrapped == nil {
|
||||
t.Fatal("Expected Logged to return non-nil handler")
|
||||
}
|
||||
|
||||
// Test that the wrapped handler works
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraced(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a simple handler
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Wrap with tracing
|
||||
wrapped := Traced(inner)
|
||||
if wrapped == nil {
|
||||
t.Fatal("Expected Traced to return non-nil handler")
|
||||
}
|
||||
|
||||
// Test that the wrapped handler works
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %d", w.Code)
|
||||
}
|
||||
|
||||
// Check that trace ID header was set
|
||||
traceID := w.Header().Get(common.HeaderTraceID)
|
||||
if traceID == "" {
|
||||
t.Error("Expected trace ID header to be set")
|
||||
}
|
||||
}
|
||||
@@ -1057,4 +1057,276 @@ func TestExportAuditLogsCSV(t *testing.T) {
|
||||
t.Error("Expected CSV header to contain 'id' and 'action' columns")
|
||||
}
|
||||
}
|
||||
|
||||
// Verify CSV has data rows (not just header)
|
||||
lines := strings.Split(body, "\n")
|
||||
// Should have at least header + some audit log rows + possible empty final line
|
||||
if len(lines) < 2 {
|
||||
t.Error("Expected CSV to have at least header + data rows")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitFromSubscriptionPlanWithValidPlan(t *testing.T) {
|
||||
// Use the internal trial plan which we know exists
|
||||
planService := billing.NewPlanService(nil)
|
||||
ul := &UserAuditLog{}
|
||||
|
||||
trialPlan := planService.GetInternalTrialPlan()
|
||||
priceMonthly, priceYearly := trialPlan.PriceIDs()
|
||||
|
||||
// Test with yearly price
|
||||
sub := &db.AuditLogSubscription{
|
||||
Source: string(dbgen.SubscriptionSourceInternal),
|
||||
ExternalProductID: trialPlan.ProductID(),
|
||||
ExternalPriceID: priceYearly,
|
||||
}
|
||||
|
||||
ul.initFromSubscriptionPlan(sub, planService, "production")
|
||||
|
||||
if ul.Property != "Product" {
|
||||
t.Errorf("Expected Property to be 'Product', got '%s'", ul.Property)
|
||||
}
|
||||
|
||||
if ul.Value == "" {
|
||||
t.Error("Expected Value to be set with plan name")
|
||||
}
|
||||
|
||||
// Test with monthly price if available
|
||||
if priceMonthly != "" {
|
||||
ul2 := &UserAuditLog{}
|
||||
sub2 := &db.AuditLogSubscription{
|
||||
Source: string(dbgen.SubscriptionSourceInternal),
|
||||
ExternalProductID: trialPlan.ProductID(),
|
||||
ExternalPriceID: priceMonthly,
|
||||
}
|
||||
|
||||
ul2.initFromSubscriptionPlan(sub2, planService, "production")
|
||||
|
||||
if ul2.Property != "Product" {
|
||||
t.Errorf("Expected Property to be 'Product', got '%s'", ul2.Property)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitFromOrgUserEmptyEmail(t *testing.T) {
|
||||
ul := &UserAuditLog{}
|
||||
|
||||
orgUser := &db.AuditLogOrgUser{
|
||||
OrgName: "Test Org",
|
||||
UserID: 1,
|
||||
Email: "", // Empty email
|
||||
Level: "member",
|
||||
}
|
||||
|
||||
err := ul.initFromOrgUser(nil, orgUser)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
// When email is empty, Property should just say "Member"
|
||||
if ul.Property != "Member" {
|
||||
t.Errorf("Expected Property to be 'Member', got '%s'", ul.Property)
|
||||
}
|
||||
|
||||
if ul.Resource != "Organization 'Test Org'" {
|
||||
t.Errorf("Expected Resource to be \"Organization 'Test Org'\", got '%s'", ul.Resource)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUserAuditLogForSubscriptionsTable(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
planService := billing.NewPlanService(nil)
|
||||
|
||||
server := &Server{
|
||||
PlanService: planService,
|
||||
Stage: common.StageStaging,
|
||||
}
|
||||
|
||||
log := &dbgen.AuditLog{
|
||||
ID: 1,
|
||||
UserID: db.Int(1),
|
||||
Action: dbgen.AuditLogActionCreate,
|
||||
EntityTable: db.TableNameSubscriptions,
|
||||
CreatedAt: db.Timestampz(time.Now()),
|
||||
Source: dbgen.AuditLogSourcePortal,
|
||||
NewValue: mustMarshalJSON(&db.AuditLogSubscription{Source: "internal", Status: "active"}),
|
||||
}
|
||||
|
||||
ul, err := server.newUserAuditLog(ctx, log)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if ul == nil {
|
||||
t.Fatal("Expected non-nil UserAuditLog")
|
||||
}
|
||||
|
||||
if ul.Resource != "Subscription" {
|
||||
t.Errorf("Expected Resource to be 'Subscription', got '%s'", ul.Resource)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUserAuditLogForOrgUsersTable(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
planService := billing.NewPlanService(nil)
|
||||
|
||||
server := &Server{
|
||||
PlanService: planService,
|
||||
Stage: common.StageStaging,
|
||||
}
|
||||
|
||||
log := &dbgen.AuditLog{
|
||||
ID: 1,
|
||||
UserID: db.Int(1),
|
||||
Action: dbgen.AuditLogActionCreate,
|
||||
EntityTable: db.TableNameOrgUsers,
|
||||
CreatedAt: db.Timestampz(time.Now()),
|
||||
Source: dbgen.AuditLogSourcePortal,
|
||||
NewValue: mustMarshalJSON(&db.AuditLogOrgUser{OrgName: "Test Org", UserID: 1, Email: "test@example.com", Level: "member"}),
|
||||
}
|
||||
|
||||
ul, err := server.newUserAuditLog(ctx, log)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if ul == nil {
|
||||
t.Fatal("Expected non-nil UserAuditLog")
|
||||
}
|
||||
|
||||
if !strings.Contains(ul.Resource, "Organization") {
|
||||
t.Errorf("Expected Resource to contain 'Organization', got '%s'", ul.Resource)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUserAuditLogsArray(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
user, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
// Create audit log events directly using PersistAuditLog
|
||||
now := time.Now().UTC()
|
||||
auditEvents := []*common.AuditLogEvent{
|
||||
{
|
||||
UserID: user.ID,
|
||||
Action: common.AuditLogActionCreate,
|
||||
EntityID: int64(user.ID),
|
||||
TableName: db.TableNameProperties,
|
||||
Timestamp: now,
|
||||
Source: common.AuditLogSourcePortal,
|
||||
NewValue: &db.AuditLogProperty{
|
||||
Name: "test-property-1.com",
|
||||
OrgID: org.ID,
|
||||
OrgName: org.Name,
|
||||
},
|
||||
},
|
||||
{
|
||||
UserID: user.ID,
|
||||
Action: common.AuditLogActionUpdate,
|
||||
EntityID: int64(user.ID),
|
||||
TableName: db.TableNameProperties,
|
||||
Timestamp: now.Add(-1 * time.Hour),
|
||||
Source: common.AuditLogSourcePortal,
|
||||
OldValue: &db.AuditLogProperty{
|
||||
Name: "old-name.com",
|
||||
},
|
||||
NewValue: &db.AuditLogProperty{
|
||||
Name: "new-name.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Cast to *db.AuditLog to access PersistAuditLog
|
||||
auditLog := store.AuditLog().(*db.AuditLog)
|
||||
if err := auditLog.PersistAuditLog(ctx, auditEvents); err != nil {
|
||||
t.Fatalf("Failed to persist audit logs: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve audit logs
|
||||
after := time.Now().UTC().AddDate(0, 0, -14)
|
||||
logs, err := store.Impl().RetrieveUserAuditLogs(ctx, user, 100, after)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve audit logs: %v", err)
|
||||
}
|
||||
|
||||
if len(logs) == 0 {
|
||||
t.Fatal("Expected non-empty audit logs array")
|
||||
}
|
||||
|
||||
// Test newUserAuditLogs with NON-EMPTY array
|
||||
result := server.newUserAuditLogs(ctx, logs)
|
||||
|
||||
if len(result) == 0 {
|
||||
t.Error("Expected non-empty result from newUserAuditLogs")
|
||||
}
|
||||
|
||||
// Verify each log has expected fields populated
|
||||
for i, ul := range result {
|
||||
if ul.Time == "" {
|
||||
t.Errorf("Audit log %d: Expected Time to be set", i)
|
||||
}
|
||||
if ul.Action == "" {
|
||||
t.Errorf("Audit log %d: Expected Action to be set", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAuditLogsContextWithAuditLogs(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
user, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
// Create audit log events directly using PersistAuditLog
|
||||
now := time.Now().UTC()
|
||||
auditEvents := []*common.AuditLogEvent{
|
||||
{
|
||||
UserID: user.ID,
|
||||
Action: common.AuditLogActionCreate,
|
||||
EntityID: int64(user.ID),
|
||||
TableName: db.TableNameProperties,
|
||||
Timestamp: now,
|
||||
Source: common.AuditLogSourcePortal,
|
||||
NewValue: &db.AuditLogProperty{
|
||||
Name: "ctx-audit-property.com",
|
||||
OrgID: org.ID,
|
||||
OrgName: org.Name,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Cast to *db.AuditLog to access PersistAuditLog
|
||||
auditLog := store.AuditLog().(*db.AuditLog)
|
||||
if err := auditLog.PersistAuditLog(ctx, auditEvents); err != nil {
|
||||
t.Fatalf("Failed to persist audit logs: %v", err)
|
||||
}
|
||||
|
||||
renderCtx, err := server.CreateAuditLogsContext(ctx, user, 14, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if renderCtx == nil {
|
||||
t.Fatal("Expected render context to be populated, got nil")
|
||||
}
|
||||
|
||||
// Should have non-empty audit logs
|
||||
if renderCtx.Count == 0 {
|
||||
t.Error("Expected Count to be > 0 after persisting audit logs")
|
||||
}
|
||||
|
||||
if len(renderCtx.AuditLogs) == 0 {
|
||||
t.Error("Expected AuditLogs to have entries")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"golang.org/x/net/html"
|
||||
|
||||
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/common"
|
||||
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/db"
|
||||
db_tests "github.com/PrivateCaptcha/PrivateCaptcha/pkg/db/tests"
|
||||
portal_tests "github.com/PrivateCaptcha/PrivateCaptcha/pkg/portal/tests"
|
||||
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/session"
|
||||
@@ -364,3 +365,91 @@ func TestLogout(t *testing.T) {
|
||||
t.Errorf("session should be destroyed after logout: got error %v, want %v", err, session.ErrSessionMissing)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortalPropertyOwnerSourceOwnerID(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
user, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
// Create a property
|
||||
property, _, err := store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, "owner-source-test.com"), org)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create property: %v", err)
|
||||
}
|
||||
|
||||
// Create the owner source
|
||||
ownerSource := &portalPropertyOwnerSource{
|
||||
Store: store,
|
||||
Sitekey: db.UUIDToSiteKey(property.ExternalID),
|
||||
}
|
||||
|
||||
// Test OwnerID
|
||||
ownerID, orgID, err := ownerSource.OwnerID(ctx, time.Now().UTC())
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if ownerID != user.ID {
|
||||
t.Errorf("Expected ownerID %d, got %d", user.ID, ownerID)
|
||||
}
|
||||
|
||||
if orgID == nil {
|
||||
t.Fatal("Expected orgID to be non-nil")
|
||||
}
|
||||
|
||||
if *orgID != org.ID {
|
||||
t.Errorf("Expected orgID %d, got %d", org.ID, *orgID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortalPropertyOwnerSourceOwnerIDNotFound(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
// Create the owner source with non-existent sitekey
|
||||
ownerSource := &portalPropertyOwnerSource{
|
||||
Store: store,
|
||||
Sitekey: "non-existent-sitekey-123456",
|
||||
}
|
||||
|
||||
// Test OwnerID should fail
|
||||
_, _, err := ownerSource.OwnerID(ctx, time.Now().UTC())
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existent sitekey")
|
||||
}
|
||||
|
||||
if err != errPortalPropertyNotFound {
|
||||
t.Errorf("Expected errPortalPropertyNotFound, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostLoginParseFormFail(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
// Create a request with invalid URL-encoded form data that will fail ParseForm
|
||||
// Using %ZZ which is an invalid percent-encoding
|
||||
req := httptest.NewRequest("POST", "/"+common.LoginEndpoint, strings.NewReader("email=%ZZ"))
|
||||
req.Header.Set(common.HeaderContentType, common.ContentTypeURLEncoded)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
|
||||
// When ParseForm fails, server redirects to error endpoint
|
||||
if w.Code != http.StatusSeeOther {
|
||||
t.Errorf("Expected redirect (303), got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2167,3 +2167,288 @@ func TestRetrieveOrgOwnerWithSubscriptionNonOwner(t *testing.T) {
|
||||
t.Error("Expected subscription to be returned for org owner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOrganizationAuditLogsWithData(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
user, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
// Create some audit logs by creating properties
|
||||
_, _, err = store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, "org-audit-1.com"), org)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create property: %v", err)
|
||||
}
|
||||
_, _, err = store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, "org-audit-2.com"), org)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create property: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve org audit logs
|
||||
logs, err := store.Impl().RetrieveOrganizationAuditLogs(ctx, org, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve org audit logs: %v", err)
|
||||
}
|
||||
|
||||
if len(logs) == 0 {
|
||||
t.Skip("No audit logs found for org - skipping test")
|
||||
}
|
||||
|
||||
// Test newOrganizationAuditLogs
|
||||
result := server.newOrganizationAuditLogs(ctx, user, logs)
|
||||
|
||||
if len(result) == 0 {
|
||||
t.Error("Expected non-empty result from newOrganizationAuditLogs")
|
||||
}
|
||||
|
||||
// Verify each log has expected fields
|
||||
for i, ul := range result {
|
||||
if ul.Time == "" {
|
||||
t.Errorf("Audit log %d: Expected Time to be set", i)
|
||||
}
|
||||
if ul.Action == "" {
|
||||
t.Errorf("Audit log %d: Expected Action to be set", i)
|
||||
}
|
||||
// UserName/UserEmail should be set (either actual name or "Unknown User")
|
||||
if ul.UserName == "" {
|
||||
t.Errorf("Audit log %d: Expected UserName to be set", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostNewOrgInvalidForm(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
user, _, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
cookie, err := portal_tests.AuthenticateSuite(ctx, user.Email, srv, server.XSRF, server.Sessions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Send invalid percent-encoding that will cause ParseForm to fail
|
||||
req := httptest.NewRequest("POST", "/org/new", strings.NewReader("name=%ZZ"))
|
||||
req.AddCookie(cookie)
|
||||
req.Header.Set(common.HeaderContentType, common.ContentTypeURLEncoded)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
|
||||
// When ParseForm fails, server redirects to error endpoint
|
||||
if w.Code != http.StatusSeeOther {
|
||||
t.Errorf("Expected redirect (303), got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostNewOrgWrongName(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
user, _, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
cookie, err := portal_tests.AuthenticateSuite(ctx, user.Email, srv, server.XSRF, server.Sessions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csrfToken := server.XSRF.Token(strconv.Itoa(int(user.ID)))
|
||||
|
||||
// Test with empty name
|
||||
form := url.Values{}
|
||||
form.Set(common.ParamCSRFToken, csrfToken)
|
||||
form.Set(common.ParamName, "")
|
||||
|
||||
req := httptest.NewRequest("POST", "/org/new", strings.NewReader(form.Encode()))
|
||||
req.AddCookie(cookie)
|
||||
req.Header.Set(common.HeaderContentType, common.ContentTypeURLEncoded)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
|
||||
// Should return 200 with error in form
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %d", w.Code)
|
||||
}
|
||||
|
||||
body := w.Body.String()
|
||||
// Check for specific error message from common.StatusOrgNameEmptyError constant
|
||||
expectedError := common.StatusOrgNameEmptyError.String()
|
||||
if !strings.Contains(body, expectedError) {
|
||||
t.Errorf("Expected error message '%s', got body: %s", expectedError, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostNewOrgUserWithoutSubscription(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
// Create a bare account without subscription
|
||||
user, _, err := db_tests.CreateNewBareAccount(ctx, store, t.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create bare account: %v", err)
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
cookie, err := portal_tests.AuthenticateSuite(ctx, user.Email, srv, server.XSRF, server.Sessions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csrfToken := server.XSRF.Token(strconv.Itoa(int(user.ID)))
|
||||
|
||||
// Attempt to create org
|
||||
form := url.Values{}
|
||||
form.Set(common.ParamCSRFToken, csrfToken)
|
||||
form.Set(common.ParamName, "Test Org Without Subscription")
|
||||
|
||||
req := httptest.NewRequest("POST", "/org/new", strings.NewReader(form.Encode()))
|
||||
req.AddCookie(cookie)
|
||||
req.Header.Set(common.HeaderContentType, common.ContentTypeURLEncoded)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
|
||||
// Should return 200 with error message about subscription
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %d", w.Code)
|
||||
}
|
||||
|
||||
body := w.Body.String()
|
||||
// Check for specific error message from activeSubscriptionForOrgError
|
||||
if !strings.Contains(body, "You need an active subscription to create new organizations") {
|
||||
t.Errorf("Expected error message about subscription requirement, got body: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteOrgMembersUnauthorized(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
// Try to delete org members without being authenticated
|
||||
req := httptest.NewRequest("DELETE", "/org/test-org-id/members/test-user-id", nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
|
||||
// Should redirect to login/error page when unauthenticated
|
||||
if w.Code != http.StatusSeeOther {
|
||||
t.Errorf("Expected redirect (303), got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteOrgMembersInvalidForm(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
user, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
cookie, err := portal_tests.AuthenticateSuite(ctx, user.Email, srv, server.XSRF, server.Sessions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csrfToken := server.XSRF.Token(strconv.Itoa(int(user.ID)))
|
||||
|
||||
// Test with invalid user ID
|
||||
req := httptest.NewRequest("DELETE", fmt.Sprintf("/org/%s/members/invalid-user-id", server.IDHasher.Encrypt(int(org.ID))), nil)
|
||||
req.AddCookie(cookie)
|
||||
req.Header.Set(common.HeaderCSRFToken, csrfToken)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
|
||||
// Should redirect to error page when invalid path argument is provided
|
||||
if w.Code != http.StatusSeeOther {
|
||||
t.Errorf("Expected redirect (303), got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteOrgMembersMemberNotOwner(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
owner, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name()+"_owner", testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create owner account: %v", err)
|
||||
}
|
||||
|
||||
member, _, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name()+"_member", testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create member account: %v", err)
|
||||
}
|
||||
|
||||
// Add member to org
|
||||
_, err = store.Impl().InviteUserToOrg(ctx, owner, org, member)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = store.Impl().JoinOrg(ctx, org.ID, member)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
// Authenticate as the member (not owner)
|
||||
cookie, err := portal_tests.AuthenticateSuite(ctx, member.Email, srv, server.XSRF, server.Sessions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csrfToken := server.XSRF.Token(strconv.Itoa(int(member.ID)))
|
||||
|
||||
// Try to remove owner (member cannot do this)
|
||||
req := httptest.NewRequest("DELETE", fmt.Sprintf("/org/%s/members/%s", server.IDHasher.Encrypt(int(org.ID)), server.IDHasher.Encrypt(int(owner.ID))), nil)
|
||||
req.AddCookie(cookie)
|
||||
req.Header.Set(common.HeaderCSRFToken, csrfToken)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
|
||||
// Member cannot delete others - should redirect to error
|
||||
if w.Code != http.StatusSeeOther {
|
||||
t.Errorf("Expected redirect (303), got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1484,3 +1484,326 @@ func TestOrgMemberWithNilSubscriptionCanCreateProperty(t *testing.T) {
|
||||
|
||||
runOrgMemberPropertyCreationPortalTest(t, nil)
|
||||
}
|
||||
|
||||
func TestGetPropertyDashboardAllTabs(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
user, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
property, _, err := server.Store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, "tabs-example.com"), org)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new property: %v", err)
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
cookie, err := portal_tests.AuthenticateSuite(ctx, user.Email, srv, server.XSRF, server.Sessions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tabs := []struct {
|
||||
name string
|
||||
tab string
|
||||
}{
|
||||
{"Reports", common.ReportsEndpoint},
|
||||
{"Integrations", common.IntegrationsEndpoint},
|
||||
{"Settings", common.SettingsEndpoint},
|
||||
{"Events", common.EventsEndpoint},
|
||||
{"Default", ""},
|
||||
{"Unknown", "unknown-tab"},
|
||||
}
|
||||
|
||||
for _, tc := range tabs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
path := fmt.Sprintf("/org/%s/property/%s", server.IDHasher.Encrypt(int(org.ID)), server.IDHasher.Encrypt(int(property.ID)))
|
||||
if tc.tab != "" {
|
||||
path += "?" + common.ParamTab + "=" + tc.tab
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", path, nil)
|
||||
req.AddCookie(cookie)
|
||||
req.SetPathValue(common.ParamOrg, server.IDHasher.Encrypt(int(org.ID)))
|
||||
req.SetPathValue(common.ParamProperty, server.IDHasher.Encrypt(int(property.ID)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
viewModel, err := server.getPropertyDashboard(w, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error for tab '%s', got: %v", tc.tab, err)
|
||||
}
|
||||
|
||||
if viewModel == nil {
|
||||
t.Fatalf("Expected ViewModel for tab '%s', got nil", tc.tab)
|
||||
}
|
||||
|
||||
if viewModel.View != propertyDashboardTemplate {
|
||||
t.Errorf("Expected view to be %s for tab '%s', got %s", propertyDashboardTemplate, tc.tab, viewModel.View)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPropertyAuditLogsArray(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
user, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
// Create property
|
||||
property, _, err := server.Store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, "prop-audit.com"), org)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new property: %v", err)
|
||||
}
|
||||
|
||||
// Update property to create more audit logs
|
||||
_, _, _ = server.Store.Impl().UpdateProperty(ctx, org, user, &dbgen.UpdatePropertyParams{
|
||||
ID: property.ID,
|
||||
Name: "Updated Property",
|
||||
Level: db.Int2(int16(common.DifficultyLevelMedium)),
|
||||
Growth: dbgen.DifficultyGrowthMedium,
|
||||
ValidityInterval: 6 * time.Hour,
|
||||
AllowSubdomains: false,
|
||||
AllowLocalhost: false,
|
||||
MaxReplayCount: 1,
|
||||
})
|
||||
|
||||
// Retrieve property audit logs
|
||||
logs, err := store.Impl().RetrievePropertyAuditLogs(ctx, property, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve property audit logs: %v", err)
|
||||
}
|
||||
|
||||
if len(logs) == 0 {
|
||||
t.Skip("No audit logs found for property - skipping test")
|
||||
}
|
||||
|
||||
// Test newPropertyAuditLogs
|
||||
result := server.newPropertyAuditLogs(ctx, user, logs)
|
||||
|
||||
if len(result) == 0 {
|
||||
t.Error("Expected non-empty result from newPropertyAuditLogs")
|
||||
}
|
||||
|
||||
// Verify each log has expected fields
|
||||
for i, ul := range result {
|
||||
if ul.Time == "" {
|
||||
t.Errorf("Audit log %d: Expected Time to be set", i)
|
||||
}
|
||||
if ul.UserName == "" {
|
||||
t.Errorf("Audit log %d: Expected UserName to be set", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPutPropertyCannotEdit(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
// Create owner
|
||||
owner, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name()+"_owner", testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create owner account: %v", err)
|
||||
}
|
||||
|
||||
// Create property under owner
|
||||
property, _, err := server.Store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(owner.ID, "edit-restrict.com"), org)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new property: %v", err)
|
||||
}
|
||||
|
||||
// Create non-owner member and add to org
|
||||
member, _, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name()+"_member", testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create member account: %v", err)
|
||||
}
|
||||
|
||||
_, err = store.Impl().InviteUserToOrg(ctx, owner, org, member)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = store.Impl().JoinOrg(ctx, org.ID, member)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
// Authenticate as member (not owner or property creator)
|
||||
cookie, err := portal_tests.AuthenticateSuite(ctx, member.Email, srv, server.XSRF, server.Sessions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csrfToken := server.XSRF.Token(strconv.Itoa(int(member.ID)))
|
||||
|
||||
// Try to edit property
|
||||
form := url.Values{}
|
||||
form.Set(common.ParamCSRFToken, csrfToken)
|
||||
form.Set(common.ParamName, "Updated Name By Member")
|
||||
form.Set(common.ParamDifficulty, "100")
|
||||
form.Set(common.ParamGrowth, "2")
|
||||
form.Set(common.ParamValidityInterval, "4")
|
||||
|
||||
req := httptest.NewRequest("PUT", fmt.Sprintf("/org/%s/property/%s", server.IDHasher.Encrypt(int(org.ID)), server.IDHasher.Encrypt(int(property.ID))), strings.NewReader(form.Encode()))
|
||||
req.AddCookie(cookie)
|
||||
req.Header.Set(common.HeaderContentType, common.ContentTypeURLEncoded)
|
||||
req.SetPathValue(common.ParamOrg, server.IDHasher.Encrypt(int(org.ID)))
|
||||
req.SetPathValue(common.ParamProperty, server.IDHasher.Encrypt(int(property.ID)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
viewModel, err := server.putProperty(w, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if viewModel == nil {
|
||||
t.Fatal("Expected ViewModel, got nil")
|
||||
}
|
||||
|
||||
// Should have error message about permissions
|
||||
renderCtx, ok := viewModel.Model.(*propertySettingsRenderContext)
|
||||
if !ok {
|
||||
t.Fatalf("Expected Model to be *propertySettingsRenderContext, got %T", viewModel.Model)
|
||||
}
|
||||
|
||||
if renderCtx.ErrorMessage == "" {
|
||||
t.Error("Expected ErrorMessage to be set for permission denial")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPutPropertyChangeDifficulty(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
user, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
property, _, err := server.Store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, "difficulty-test.com"), org)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new property: %v", err)
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
cookie, err := portal_tests.AuthenticateSuite(ctx, user.Email, srv, server.XSRF, server.Sessions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csrfToken := server.XSRF.Token(strconv.Itoa(int(user.ID)))
|
||||
|
||||
// Change difficulty to a new value
|
||||
newDifficulty := int(common.DifficultyLevelSmall) + 10
|
||||
form := url.Values{}
|
||||
form.Set(common.ParamCSRFToken, csrfToken)
|
||||
form.Set(common.ParamName, property.Name)
|
||||
form.Set(common.ParamDifficulty, strconv.Itoa(newDifficulty))
|
||||
form.Set(common.ParamGrowth, "2")
|
||||
form.Set(common.ParamValidityInterval, "4")
|
||||
|
||||
req := httptest.NewRequest("PUT", fmt.Sprintf("/org/%s/property/%s", server.IDHasher.Encrypt(int(org.ID)), server.IDHasher.Encrypt(int(property.ID))), strings.NewReader(form.Encode()))
|
||||
req.AddCookie(cookie)
|
||||
req.Header.Set(common.HeaderContentType, common.ContentTypeURLEncoded)
|
||||
req.SetPathValue(common.ParamOrg, server.IDHasher.Encrypt(int(org.ID)))
|
||||
req.SetPathValue(common.ParamProperty, server.IDHasher.Encrypt(int(property.ID)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
viewModel, err := server.putProperty(w, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if viewModel == nil {
|
||||
t.Fatal("Expected ViewModel, got nil")
|
||||
}
|
||||
|
||||
// Should have success message
|
||||
renderCtx, ok := viewModel.Model.(*propertySettingsRenderContext)
|
||||
if !ok {
|
||||
t.Fatalf("Expected Model to be *propertySettingsRenderContext, got %T", viewModel.Model)
|
||||
}
|
||||
|
||||
if renderCtx.SuccessMessage == "" {
|
||||
t.Error("Expected SuccessMessage to be set after updating property")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletePropertyCannotDelete(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
// Create owner
|
||||
owner, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name()+"_owner", testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create owner account: %v", err)
|
||||
}
|
||||
|
||||
// Create property under owner
|
||||
property, _, err := server.Store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(owner.ID, "delete-restrict.com"), org)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new property: %v", err)
|
||||
}
|
||||
|
||||
// Create non-owner member and add to org
|
||||
member, _, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name()+"_member", testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create member account: %v", err)
|
||||
}
|
||||
|
||||
_, err = store.Impl().InviteUserToOrg(ctx, owner, org, member)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = store.Impl().JoinOrg(ctx, org.ID, member)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
// Authenticate as member (not owner or property creator)
|
||||
cookie, err := portal_tests.AuthenticateSuite(ctx, member.Email, srv, server.XSRF, server.Sessions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csrfToken := server.XSRF.Token(strconv.Itoa(int(member.ID)))
|
||||
|
||||
// Try to delete property
|
||||
req := httptest.NewRequest("DELETE", fmt.Sprintf("/org/%s/property/%s", server.IDHasher.Encrypt(int(org.ID)), server.IDHasher.Encrypt(int(property.ID))), nil)
|
||||
req.AddCookie(cookie)
|
||||
req.Header.Set(common.HeaderCSRFToken, csrfToken)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
|
||||
// Member cannot delete property they don't own - should return 405 Method Not Allowed
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Errorf("Expected method not allowed (405), got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,9 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
registerContentsTemplate = "login/register-contents.html"
|
||||
userNameErrorMessage = "Name contains invalid characters."
|
||||
registerContentsTemplate = "login/register-contents.html"
|
||||
userNameErrorMessage = "Name contains invalid characters."
|
||||
emailAlreadyRegisteredError = "Such email is already registered. Login instead?"
|
||||
)
|
||||
|
||||
func (s *Server) getRegister(w http.ResponseWriter, r *http.Request) (*ViewModel, error) {
|
||||
@@ -149,7 +150,7 @@ func (s *Server) postRegister(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := s.Store.Impl().FindUserByEmail(ctx, email); err == nil {
|
||||
slog.WarnContext(ctx, "User with such email already exists", "email", email)
|
||||
data.Email = ""
|
||||
data.EmailError = "Such email is already registered. Login instead?"
|
||||
data.EmailError = emailAlreadyRegisteredError
|
||||
s.render(w, r, registerContentsTemplate, data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/common"
|
||||
db_tests "github.com/PrivateCaptcha/PrivateCaptcha/pkg/db/tests"
|
||||
portal_tests "github.com/PrivateCaptcha/PrivateCaptcha/pkg/portal/tests"
|
||||
)
|
||||
|
||||
@@ -300,3 +301,97 @@ func TestPostRegisterMissingCaptcha(t *testing.T) {
|
||||
t.Error("Expected error message about captcha")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostRegisterExistingEmail(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
// Create an existing user first
|
||||
existingUser, _, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name()+"_existing", testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create existing account: %v", err)
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
// Try to register with the same email
|
||||
form := url.Values{}
|
||||
form.Add(common.ParamCSRFToken, server.XSRF.Token(""))
|
||||
form.Add(common.ParamEmail, existingUser.Email)
|
||||
form.Add(common.ParamName, "Another User")
|
||||
form.Add(common.ParamTerms, "true")
|
||||
form.Add(common.ParamPortalSolution, "captchaSolution")
|
||||
|
||||
req := httptest.NewRequest("POST", "/"+common.RegisterEndpoint, bytes.NewBufferString(form.Encode()))
|
||||
req.Header.Set(common.HeaderContentType, common.ContentTypeURLEncoded)
|
||||
w := httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status code 200, got %v", w.Code)
|
||||
}
|
||||
|
||||
body := w.Body.String()
|
||||
// Check for specific error message constant
|
||||
if !strings.Contains(body, emailAlreadyRegisteredError) {
|
||||
t.Errorf("Expected error message '%s', got body: %s", emailAlreadyRegisteredError, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRegisterDisabled(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
// Temporarily disable registration
|
||||
server.canRegister.Store(false)
|
||||
defer server.canRegister.Store(true)
|
||||
|
||||
req := httptest.NewRequest("GET", "/"+common.RegisterEndpoint, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
viewModel, err := server.getRegister(w, req)
|
||||
|
||||
if err != errRegistrationDisabled {
|
||||
t.Errorf("Expected errRegistrationDisabled, got: %v", err)
|
||||
}
|
||||
|
||||
if viewModel != nil {
|
||||
t.Error("Expected nil ViewModel when registration is disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostRegisterDisabled(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
// Temporarily disable registration
|
||||
server.canRegister.Store(false)
|
||||
defer server.canRegister.Store(true)
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
// Try to register
|
||||
form := url.Values{}
|
||||
form.Add(common.ParamCSRFToken, server.XSRF.Token(""))
|
||||
form.Add(common.ParamEmail, "disabled-registration@privatecaptcha.com")
|
||||
form.Add(common.ParamName, "Test User")
|
||||
form.Add(common.ParamTerms, "true")
|
||||
form.Add(common.ParamPortalSolution, "captchaSolution")
|
||||
|
||||
req := httptest.NewRequest("POST", "/"+common.RegisterEndpoint, bytes.NewBufferString(form.Encode()))
|
||||
req.Header.Set(common.HeaderContentType, common.ContentTypeURLEncoded)
|
||||
w := httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
|
||||
// Should be redirected to an error page when registration is disabled
|
||||
if w.Code != http.StatusSeeOther {
|
||||
t.Errorf("Expected redirect status when registration disabled, got %v", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package portal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
|
||||
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/common"
|
||||
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/db"
|
||||
dbgen "github.com/PrivateCaptcha/PrivateCaptcha/pkg/db/generated"
|
||||
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/db/tests"
|
||||
db_tests "github.com/PrivateCaptcha/PrivateCaptcha/pkg/db/tests"
|
||||
portal_tests "github.com/PrivateCaptcha/PrivateCaptcha/pkg/portal/tests"
|
||||
@@ -519,18 +521,21 @@ func TestDeleteAccount(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAccountStats(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
type accountStatsSuiteResult struct {
|
||||
user *dbgen.User
|
||||
srv *http.ServeMux
|
||||
cookie *http.Cookie
|
||||
}
|
||||
|
||||
func accountStatsSuite(t *testing.T, ctx context.Context) *accountStatsSuiteResult {
|
||||
t.Helper()
|
||||
|
||||
ctx := common.TraceContext(t.Context(), t.Name())
|
||||
user, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
property, _, err := server.Store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, "stats-example.com"), org)
|
||||
property, _, err := server.Store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, t.Name()+".com"), org)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new property: %v", err)
|
||||
}
|
||||
@@ -571,11 +576,22 @@ func TestGetAccountStats(t *testing.T) {
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
return &accountStatsSuiteResult{user: user, srv: srv, cookie: cookie}
|
||||
}
|
||||
|
||||
func TestGetAccountStats(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := common.TraceContext(t.Context(), t.Name())
|
||||
suite := accountStatsSuite(t, ctx)
|
||||
|
||||
req := httptest.NewRequest("GET", "/user/stats", nil)
|
||||
req.AddCookie(cookie)
|
||||
req.AddCookie(suite.cookie)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
suite.srv.ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -596,10 +612,6 @@ func TestGetAccountStats(t *testing.T) {
|
||||
t.Fatalf("Expected 1 series, got %d", len(stats.Series))
|
||||
}
|
||||
|
||||
if stats.Series[0].Name != org.Name {
|
||||
t.Errorf("Expected series name %q, got %q", org.Name, stats.Series[0].Name)
|
||||
}
|
||||
|
||||
totalCount := 0
|
||||
for _, p := range stats.Data {
|
||||
totalCount += p.Value
|
||||
@@ -923,3 +935,233 @@ func TestParseAPIKeyScope(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPutGeneralSettingsChangeName(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := common.TraceContext(t.Context(), t.Name())
|
||||
user, _, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
cookie, err := portal_tests.AuthenticateSuite(ctx, user.Email, srv, server.XSRF, server.Sessions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Change name to something different
|
||||
newName := "Updated Name"
|
||||
form := url.Values{}
|
||||
form.Set(common.ParamCSRFToken, server.XSRF.Token(strconv.Itoa(int(user.ID))))
|
||||
form.Set(common.ParamName, newName)
|
||||
|
||||
req := httptest.NewRequest("PUT", "/settings/tab/general", strings.NewReader(form.Encode()))
|
||||
req.AddCookie(cookie)
|
||||
req.Header.Set(common.HeaderContentType, common.ContentTypeURLEncoded)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
viewModel, err := server.putGeneralSettings(w, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if viewModel == nil {
|
||||
t.Fatal("Expected ViewModel to be populated, got nil")
|
||||
}
|
||||
|
||||
renderCtx, ok := viewModel.Model.(*settingsGeneralRenderContext)
|
||||
if !ok {
|
||||
t.Fatalf("Expected Model to be *settingsGeneralRenderContext, got %T", viewModel.Model)
|
||||
}
|
||||
|
||||
// Check that name was updated
|
||||
if renderCtx.Name != newName {
|
||||
t.Errorf("Expected Name to be '%s', got '%s'", newName, renderCtx.Name)
|
||||
}
|
||||
|
||||
// Verify in DB
|
||||
updatedUser, err := store.Impl().RetrieveUser(ctx, user.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if updatedUser.Name != newName {
|
||||
t.Errorf("Expected user name in DB to be '%s', got '%s'", newName, updatedUser.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostAPIKeySettingsScopedKey(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := common.TraceContext(t.Context(), t.Name())
|
||||
user, org, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
cookie, err := portal_tests.AuthenticateSuite(ctx, user.Email, srv, server.XSRF, server.Sessions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csrfToken := server.XSRF.Token(strconv.Itoa(int(user.ID)))
|
||||
|
||||
// Create a scoped API key for a specific org
|
||||
form := url.Values{}
|
||||
form.Set(common.ParamCSRFToken, csrfToken)
|
||||
form.Set(common.ParamName, "Scoped API Key")
|
||||
form.Set(common.ParamDays, "90")
|
||||
form.Set(common.ParamScope, apiKeyScopePortal+apiKeyReadWriteSuffix)
|
||||
form.Set(common.ParamOrg, server.IDHasher.Encrypt(int(org.ID)))
|
||||
|
||||
req := httptest.NewRequest("POST", "/settings/tab/apikeys/new", strings.NewReader(form.Encode()))
|
||||
req.AddCookie(cookie)
|
||||
req.Header.Set(common.HeaderContentType, common.ContentTypeURLEncoded)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Unexpected status code %v", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Check that the API key was created with org scope
|
||||
keys, err := store.Impl().RetrieveUserAPIKeys(ctx, user.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
t.Error("Expected API key to be created")
|
||||
}
|
||||
|
||||
// Find the scoped key
|
||||
var foundScopedKey bool
|
||||
for _, key := range keys {
|
||||
if key.Name == "Scoped API Key" && key.OrgID.Valid && key.OrgID.Int32 == org.ID {
|
||||
foundScopedKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundScopedKey {
|
||||
t.Error("Expected to find a scoped API key for the org")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAccountStatsWithUnknownOrg(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := common.TraceContext(t.Context(), t.Name())
|
||||
user, org1, err := db_tests.CreateNewAccountForTest(ctx, store, t.Name(), testPlan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
|
||||
property1, _, err := server.Store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, "stats-unknown-1.com"), org1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new property: %v", err)
|
||||
}
|
||||
|
||||
// Create second org
|
||||
org2, _, err := store.Impl().CreateNewOrganization(ctx, "Second Org For Deletion", user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create second org: %v", err)
|
||||
}
|
||||
|
||||
property2, _, err := server.Store.Impl().CreateNewProperty(ctx, db_tests.CreateNewPropertyParams(user.ID, "stats-unknown-2.com"), org2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new property: %v", err)
|
||||
}
|
||||
|
||||
srv := http.NewServeMux()
|
||||
server.Setup(portalDomain(), common.NoopMiddleware).Register(srv)
|
||||
|
||||
cookie, err := portal_tests.AuthenticateSuite(ctx, user.Email, srv, server.XSRF, server.Sessions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
accessRecords := []*common.AccessRecord{
|
||||
{
|
||||
UserID: user.ID,
|
||||
OrgID: org1.ID,
|
||||
PropertyID: property1.ID,
|
||||
Timestamp: now.Add(-1 * time.Hour),
|
||||
},
|
||||
{
|
||||
UserID: user.ID,
|
||||
OrgID: org2.ID,
|
||||
PropertyID: property2.ID,
|
||||
Timestamp: now.Add(-2 * time.Hour),
|
||||
},
|
||||
}
|
||||
|
||||
if err := timeSeries.WriteAccessLogBatch(ctx, accessRecords); err != nil {
|
||||
t.Fatalf("Failed to write access log batch: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Delete the second org BEFORE retrieving stats
|
||||
if _, err := store.Impl().SoftDeleteOrganization(ctx, org2, user); err != nil {
|
||||
t.Fatalf("Failed to delete org: %v", err)
|
||||
}
|
||||
|
||||
// Now get stats - should have one org with "Unknown Organization" name
|
||||
req := httptest.NewRequest("GET", "/user/stats", nil)
|
||||
req.AddCookie(cookie)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
srv.ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Unexpected status code %v", resp.StatusCode)
|
||||
}
|
||||
|
||||
var stats accountStatsResponse
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
// Should have stats
|
||||
if len(stats.Data) == 0 {
|
||||
t.Error("Expected data but got none")
|
||||
}
|
||||
|
||||
// Should have at least one series (for the existing org)
|
||||
if len(stats.Series) == 0 {
|
||||
t.Error("Expected at least one series")
|
||||
}
|
||||
|
||||
// Check if any series has "Unknown" in name (for deleted org)
|
||||
hasUnknown := false
|
||||
for _, series := range stats.Series {
|
||||
if strings.Contains(series.Name, "Unknown") {
|
||||
hasUnknown = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasUnknown {
|
||||
t.Log("Note: Deleted org may have been filtered out if stats only returns current user's orgs")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,25 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// limitedWriter is an io.Writer that returns an error after writing N bytes
|
||||
type limitedWriter struct {
|
||||
limit int
|
||||
written int
|
||||
}
|
||||
|
||||
func (w *limitedWriter) Write(p []byte) (int, error) {
|
||||
if w.written >= w.limit {
|
||||
return 0, io.ErrShortWrite
|
||||
}
|
||||
remaining := w.limit - w.written
|
||||
if len(p) <= remaining {
|
||||
w.written += len(p)
|
||||
return len(p), nil
|
||||
}
|
||||
w.written += remaining
|
||||
return remaining, io.ErrShortWrite
|
||||
}
|
||||
|
||||
func randInit(data []byte) {
|
||||
for i := range data {
|
||||
data[i] = byte(rand.Intn(256))
|
||||
@@ -252,3 +271,123 @@ func TestValidityIntervalToIndex(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputePuzzleWriteToErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
propertyID := [16]byte{}
|
||||
randInit(propertyID[:])
|
||||
|
||||
puzzle := NewComputePuzzle(NextPuzzleID(), propertyID, 123)
|
||||
_ = puzzle.Init(DefaultValidityPeriod)
|
||||
|
||||
// Test error at version write (byte 0)
|
||||
t.Run("ErrorAtVersion", func(t *testing.T) {
|
||||
w := &limitedWriter{limit: 0}
|
||||
_, err := puzzle.WriteTo(w)
|
||||
if err == nil {
|
||||
t.Error("Expected error at version write")
|
||||
}
|
||||
})
|
||||
|
||||
// Test error at propertyID write (after 1 byte)
|
||||
t.Run("ErrorAtPropertyID", func(t *testing.T) {
|
||||
w := &limitedWriter{limit: 1}
|
||||
_, err := puzzle.WriteTo(w)
|
||||
if err == nil {
|
||||
t.Error("Expected error at propertyID write")
|
||||
}
|
||||
})
|
||||
|
||||
// Test error at puzzleID write (after 1 + 16 = 17 bytes)
|
||||
t.Run("ErrorAtPuzzleID", func(t *testing.T) {
|
||||
w := &limitedWriter{limit: 17}
|
||||
_, err := puzzle.WriteTo(w)
|
||||
if err == nil {
|
||||
t.Error("Expected error at puzzleID write")
|
||||
}
|
||||
})
|
||||
|
||||
// Test error at difficulty write (after 1 + 16 + 8 = 25 bytes)
|
||||
t.Run("ErrorAtDifficulty", func(t *testing.T) {
|
||||
w := &limitedWriter{limit: 25}
|
||||
_, err := puzzle.WriteTo(w)
|
||||
if err == nil {
|
||||
t.Error("Expected error at difficulty write")
|
||||
}
|
||||
})
|
||||
|
||||
// Test error at solutionsCount write (after 1 + 16 + 8 + 1 = 26 bytes)
|
||||
t.Run("ErrorAtSolutionsCount", func(t *testing.T) {
|
||||
w := &limitedWriter{limit: 26}
|
||||
_, err := puzzle.WriteTo(w)
|
||||
if err == nil {
|
||||
t.Error("Expected error at solutionsCount write")
|
||||
}
|
||||
})
|
||||
|
||||
// Test error at expiration write (after 1 + 16 + 8 + 1 + 1 = 27 bytes)
|
||||
t.Run("ErrorAtExpiration", func(t *testing.T) {
|
||||
w := &limitedWriter{limit: 27}
|
||||
_, err := puzzle.WriteTo(w)
|
||||
if err == nil {
|
||||
t.Error("Expected error at expiration write")
|
||||
}
|
||||
})
|
||||
|
||||
// Test error at userData write (after 1 + 16 + 8 + 1 + 1 + 4 = 31 bytes)
|
||||
t.Run("ErrorAtUserData", func(t *testing.T) {
|
||||
w := &limitedWriter{limit: 31}
|
||||
_, err := puzzle.WriteTo(w)
|
||||
if err == nil {
|
||||
t.Error("Expected error at userData write")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPuzzlePayloadWriteErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
propertyID := [16]byte{}
|
||||
randInit(propertyID[:])
|
||||
p := NewComputePuzzle(NextPuzzleID(), propertyID, 0 /*difficulty*/)
|
||||
_ = p.Init(DefaultValidityPeriod)
|
||||
|
||||
salt := NewSalt([]byte("salt"))
|
||||
puzzleData, err := p.Serialize(ctx, salt, nil /*property salt*/)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test error at puzzleBase64 write
|
||||
t.Run("ErrorAtPuzzleBase64", func(t *testing.T) {
|
||||
w := &limitedWriter{limit: 0}
|
||||
err := puzzleData.Write(w)
|
||||
if err == nil {
|
||||
t.Error("Expected error at puzzleBase64 write")
|
||||
}
|
||||
})
|
||||
|
||||
// Test error at signatureBase64 write (after puzzleBase64 + dotBytes)
|
||||
t.Run("ErrorAtSignatureBase64", func(t *testing.T) {
|
||||
w := &limitedWriter{limit: len(puzzleData.puzzleBase64) + len(dotBytes)}
|
||||
err := puzzleData.Write(w)
|
||||
if err == nil {
|
||||
t.Error("Expected error at signatureBase64 write")
|
||||
}
|
||||
})
|
||||
|
||||
// Test successful write
|
||||
t.Run("SuccessfulWrite", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
err := puzzleData.Write(&buf)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
if buf.Len() != puzzleData.Size() {
|
||||
t.Errorf("Expected size %d, got %d", puzzleData.Size(), buf.Len())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package puzzle
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -61,3 +62,230 @@ func TestZeroDifficulty(t *testing.T) {
|
||||
t.Errorf("Zero difficulty should suffice. Solutions count %v, expected %v", count, puzzle.SolutionsCount())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetadataMethods(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
metadata *Metadata
|
||||
wantWasmFlag bool
|
||||
wantErrorCode uint8
|
||||
wantElapsed uint32
|
||||
}{
|
||||
{
|
||||
name: "NilMetadata",
|
||||
metadata: nil,
|
||||
wantWasmFlag: false,
|
||||
wantErrorCode: 0,
|
||||
wantElapsed: 0,
|
||||
},
|
||||
{
|
||||
name: "WasmFlagTrue",
|
||||
metadata: &Metadata{wasmFlag: true, errorCode: 0, elapsedMillis: 0},
|
||||
wantWasmFlag: true,
|
||||
wantErrorCode: 0,
|
||||
wantElapsed: 0,
|
||||
},
|
||||
{
|
||||
name: "AllFieldsSet",
|
||||
metadata: &Metadata{wasmFlag: true, errorCode: 42, elapsedMillis: 12345},
|
||||
wantWasmFlag: true,
|
||||
wantErrorCode: 42,
|
||||
wantElapsed: 12345,
|
||||
},
|
||||
{
|
||||
name: "WasmFlagFalse",
|
||||
metadata: &Metadata{wasmFlag: false, errorCode: 0, elapsedMillis: 0},
|
||||
wantWasmFlag: false,
|
||||
wantErrorCode: 0,
|
||||
wantElapsed: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.metadata.WasmFlag(); got != tt.wantWasmFlag {
|
||||
t.Errorf("WasmFlag() = %v, want %v", got, tt.wantWasmFlag)
|
||||
}
|
||||
if got := tt.metadata.ErrorCode(); got != tt.wantErrorCode {
|
||||
t.Errorf("ErrorCode() = %v, want %v", got, tt.wantErrorCode)
|
||||
}
|
||||
if got := tt.metadata.ElapsedMillis(); got != tt.wantElapsed {
|
||||
t.Errorf("ElapsedMillis() = %v, want %v", got, tt.wantElapsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSolutionsErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
expectError bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "EmptyData",
|
||||
data: []byte{},
|
||||
expectError: true,
|
||||
errorMsg: "empty encoded solutions",
|
||||
},
|
||||
{
|
||||
name: "NilData",
|
||||
data: nil,
|
||||
expectError: true,
|
||||
errorMsg: "empty encoded solutions",
|
||||
},
|
||||
{
|
||||
name: "InvalidBase64",
|
||||
data: []byte("not-valid-base64!!!"),
|
||||
expectError: true,
|
||||
errorMsg: "invalid base64",
|
||||
},
|
||||
{
|
||||
name: "InvalidVersion",
|
||||
data: []byte(base64.StdEncoding.EncodeToString([]byte{2, 0, 0, 0, 0, 0, 0})), // version 2 is invalid
|
||||
expectError: true,
|
||||
errorMsg: "invalid version",
|
||||
},
|
||||
{
|
||||
name: "InvalidSolutionLength",
|
||||
data: []byte(base64.StdEncoding.EncodeToString([]byte{1, 0, 0, 0, 0, 0, 0, 1, 2, 3})), // 3 bytes is not multiple of 8
|
||||
expectError: true,
|
||||
errorMsg: "not SolutionLength multiple",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := NewSolutions(tt.data)
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error containing '%s', got nil", tt.errorMsg)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSolutionsValidData(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create valid metadata + solutions data
|
||||
metadata := &Metadata{
|
||||
errorCode: 0,
|
||||
wasmFlag: true,
|
||||
elapsedMillis: 1000,
|
||||
}
|
||||
|
||||
metadataBytes, err := metadata.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Add one valid solution (8 bytes)
|
||||
solutionBytes := make([]byte, SolutionLength)
|
||||
for i := range solutionBytes {
|
||||
solutionBytes[i] = byte(i)
|
||||
}
|
||||
|
||||
fullData := append(metadataBytes, solutionBytes...)
|
||||
encodedData := []byte(base64.StdEncoding.EncodeToString(fullData))
|
||||
|
||||
solutions, err := NewSolutions(encodedData)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if solutions == nil {
|
||||
t.Fatal("Expected solutions to be non-nil")
|
||||
}
|
||||
|
||||
if !solutions.Metadata.WasmFlag() {
|
||||
t.Error("Expected WasmFlag to be true")
|
||||
}
|
||||
|
||||
if solutions.Metadata.ElapsedMillis() != 1000 {
|
||||
t.Errorf("Expected ElapsedMillis to be 1000, got %d", solutions.Metadata.ElapsedMillis())
|
||||
}
|
||||
|
||||
if len(solutions.Buffer) != SolutionLength {
|
||||
t.Errorf("Expected buffer length %d, got %d", SolutionLength, len(solutions.Buffer))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetadataUnmarshalBinaryErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "TooShort",
|
||||
data: []byte{1, 2, 3},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidVersion",
|
||||
data: []byte{2, 0, 0, 0, 0, 0, 0},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "ValidData",
|
||||
data: []byte{1, 0, 1, 0, 0, 0, 0}, // version=1, errorCode=0, wasmFlag=1, elapsedMillis=0
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := &Metadata{}
|
||||
err := m.UnmarshalBinary(tt.data)
|
||||
if tt.expectError && err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
if !tt.expectError && err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSolutionsString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
solutions := emptySolutions(1)
|
||||
str := solutions.String()
|
||||
|
||||
if len(str) == 0 {
|
||||
t.Error("Expected non-empty string")
|
||||
}
|
||||
|
||||
// Verify it's valid base64
|
||||
_, err := base64.StdEncoding.DecodeString(str)
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid base64 string, got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSolutionsVerifyInvalidPuzzleBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := t.Context()
|
||||
solutions := &Solutions{Buffer: make([]byte, SolutionLength)}
|
||||
|
||||
// Test with invalid puzzle bytes length
|
||||
_, err := solutions.Verify(ctx, make([]byte, 10), 100)
|
||||
if err != ErrInvalidPuzzleBytes {
|
||||
t.Errorf("Expected ErrInvalidPuzzleBytes, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user