Ctrl K
ComunidadGuías…

internal/repository/clickhouse/meter_usage_cumulative_integration_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package clickhouse

import (
	"context"
	"os"
	"testing"
	"time"

	"github.com/flexprice/flexprice/internal/clickhouse"
	"github.com/flexprice/flexprice/internal/config"
	"github.com/flexprice/flexprice/internal/domain/events"
	"github.com/flexprice/flexprice/internal/logger"
	"github.com/flexprice/flexprice/internal/types"
	"github.com/google/uuid"
	"github.com/stretchr/testify/require"
)

// newRealClickHouseTestStore builds a real *clickhouse.ClickHouseStore backed
// by an actual ClickHouse instance. Configuration comes from
// FLEXPRICE_TEST_CLICKHOUSE_* env vars (defaults match the docker-compose dev
// stack), mirroring internal/repository/ent/revenue_fact_test.go's harness.
//
// Skips (instead of failing) when no reachable ClickHouse is configured, so it
// does not break `make test` / CI runs without a live database.
func newRealClickHouseTestStore(t *testing.T) *clickhouse.ClickHouseStore {
	t.Helper()

	address := chEnvOrDefault("FLEXPRICE_TEST_CLICKHOUSE_ADDRESS", "localhost:9000")
	username := chEnvOrDefault("FLEXPRICE_TEST_CLICKHOUSE_USERNAME", "flexprice")
	password := chEnvOrDefault("FLEXPRICE_TEST_CLICKHOUSE_PASSWORD", "flexprice123")
	database := chEnvOrDefault("FLEXPRICE_TEST_CLICKHOUSE_DATABASE", "flexprice")

	cfg := &config.Configuration{
		ClickHouse: config.ClickHouseConfig{
			Address:        address,
			Username:       username,
			Password:       password,
			Database:       database,
			MaxMemoryUsage: 1,
		},
	}

	store, err := clickhouse.NewClickHouseStore(cfg, nil)
	if err != nil {
		t.Skipf("skipping: real ClickHouse test instance not reachable at %s (%v)", address, err)
		return nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	if err := store.GetConn().Ping(ctx); err != nil {
		t.Skipf("skipping: real ClickHouse test instance not reachable at %s (%v)", address, err)
		return nil
	}

	t.Cleanup(func() {
		_ = store.Close()
	})

	return store
}

func chEnvOrDefault(key, def string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return def
}

// TestGetDailyUsageByMeter_Integration proves GetDailyUsageByMeter runs against
// a real ClickHouse instance and keys its per-day rows by meter. Skips
// automatically when ClickHouse isn't reachable (see
// newRealClickHouseTestStore) — this test's job is to compile and exercise real
// wiring, not to assert on seeded data.
func TestGetDailyUsageByMeter_Integration(t *testing.T) {
	store := newRealClickHouseTestStore(t)

	log, err := logger.NewLogger(&config.Configuration{
		Logging: config.LoggingConfig{Level: types.LogLevelInfo},
	})
	require.NoError(t, err)

	repo := NewMeterUsageRepository(store, log)

	meterA := "test_meter_" + uuid.NewString()
	meterB := "test_meter_" + uuid.NewString()
	params := &events.DailyUsageParams{
		TenantID:      "test_tenant_" + uuid.NewString(),
		EnvironmentID: "test_env_" + uuid.NewString(),
		MeterIDs:      []string{meterA, meterB},
		StartTime:     time.Now().UTC().AddDate(0, 0, -30),
		EndTime:       time.Now().UTC(),
		UseFinal:      true,
	}

	byMeter, err := repo.GetDailyUsageByMeter(context.Background(), params)
	require.NoError(t, err)

	// Whatever comes back must be attributed to a meter we asked for, and the
	// days within a meter must be ordered — the caller accumulates in order.
	for meterID, points := range byMeter {
		require.Contains(t, params.MeterIDs, meterID, "a meter we did not ask for came back")
		for i := 1; i < len(points); i++ {
			require.False(t, points[i].Day.Before(points[i-1].Day), "days must be ordered within a meter")
		}
	}

	// An empty meter list must not issue a query at all.
	empty, err := repo.GetDailyUsageByMeter(context.Background(), &events.DailyUsageParams{
		TenantID: params.TenantID, EnvironmentID: params.EnvironmentID,
	})
	require.NoError(t, err)
	require.Empty(t, empty)
}

func TestGetUsageMultiMeterBillingGroups_Integration(t *testing.T) {
	store := newRealClickHouseTestStore(t)
	repo := &MeterUsageRepository{
		store: store,
		qb:    NewMeterUsageQueryBuilder(),
	}
	params := &events.MeterUsageQueryParams{
		TenantID:            "batch_tenant",
		EnvironmentID:       "batch_env",
		ExternalCustomerIDs: []string{"batch_customer"},
		MeterIDs:            []string{"meter_a", "meter_b"},
		AggregationType:     types.AggregationMax,
		WindowSize:          types.WindowSizeDay,
		GroupBy:             []string{"properties.instance"},
		StartTime:           time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
		EndTime:             time.Date(2026, 1, 3, 0, 0, 0, 0, time.UTC),
	}
	// A CTE fixture exercises the real query and scanner without writing shared tables.
	fixture := `WITH meter_usage AS (
		SELECT row.1 AS meter_id, row.2 AS id, toJSONString(map('instance', row.3)) AS properties, toDecimal128(row.4, 8) AS qty_total,
			toDateTime('2026-01-01 12:00:00', 'UTC') AS timestamp,
			'batch_tenant' AS tenant_id, row.5 AS environment_id, row.6 AS external_customer_id
		FROM (SELECT arrayJoin([
			('meter_a', 'a1', 'x', 60, 'batch_env', 'batch_customer'),
			('meter_a', 'a2', 'x', 120, 'batch_env', 'batch_customer'),
			('meter_a', 'a3', 'y', 180, 'batch_env', 'batch_customer'),
			('meter_b', 'b1', 'x', 70, 'batch_env', 'batch_customer'),
			('meter_a', 'other_env', 'x', 9999, 'other_env', 'batch_customer'),
			('meter_a', 'other_customer', 'x', 9999, 'batch_env', 'other_customer')
		]) AS row)
	) `
	query := fixture + BuildMultiMeterQuery("MAX(qty_total)", "COUNT(DISTINCT id)", params, repo.qb)
	_, args := repo.qb.BuildWhereClause(params)
	results, err := repo.executeMultiMeterWindowedQuery(context.Background(), query, args, params)
	require.NoError(t, err)
	require.Len(t, results, 2)
	for _, result := range results {
		switch result.MeterID {
		case "meter_a":
			require.Equal(t, "300", result.TotalValue.String())
			require.Len(t, result.Points, 2)
			require.Equal(t, "120", result.Points[0].Value.String())
			require.Equal(t, "180", result.Points[1].Value.String())
		case "meter_b":
			require.Equal(t, "70", result.TotalValue.String())
			require.Len(t, result.Points, 1)
		default:
			t.Fatalf("unexpected meter %s", result.MeterID)
		}
	}
}