//go:build integration // +build integration package integration import ( "context" "database/sql" "fmt" "net" "sync" "testing" "time" "github.com/jackc/pgx/v5/pgxpool" _ "github.com/jackc/pgx/v5/stdlib" "github.com/ozontech/allure-go/pkg/framework/provider" "github.com/ozontech/allure-go/pkg/framework/suite" "github.com/pressly/goose/v3" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" "route256/loms/internal/app/server" "route256/loms/internal/domain/entity" ordersRepository "route256/loms/internal/domain/repository/orders/sqlc" stocksRepository "route256/loms/internal/domain/repository/stocks/sqlc" lomsService "route256/loms/internal/domain/service" "route256/loms/internal/infra/postgres" pb "route256/pkg/api/loms/v1" ) const ( // "total_count": 100, // "reserved": 35 testSKU = entity.Sku(1076963) testUID = entity.ID(1337) testCount = uint32(2) migrationsDir = "../../db/migrations" ) // TODO: drop, use actual kafka for tests. type mockKafkaProducer struct{} func (kp mockKafkaProducer) Send(_ context.Context, id entity.ID, status string) error { return nil } func startPostgres(ctx context.Context, migrationsDir string) (*pgxpool.Pool, func(), error) { req := testcontainers.ContainerRequest{ Image: "gitlab-registry.ozon.dev/go/classroom-18/students/base/postgres:16", Env: map[string]string{ "POSTGRESQL_USERNAME": "user", "POSTGRESQL_PASSWORD": "postgres", "POSTGRESQL_DATABASE": "loms_test", }, ExposedPorts: []string{"5432/tcp"}, WaitingFor: wait.ForListeningPort("5432/tcp").WithStartupTimeout(30 * time.Second), } container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ContainerRequest: req, Started: true}) if err != nil { return nil, nil, err } endpoint, err := container.PortEndpoint(ctx, "5432", "") if err != nil { container.Terminate(ctx) return nil, nil, err } dsn := fmt.Sprintf("postgresql://user:postgres@%s/loms_test?sslmode=disable", endpoint) var pool *pgxpool.Pool for i := 0; i < 30; i++ { pool, err = pgxpool.New(ctx, dsn) if err == nil && pool.Ping(ctx) == nil { break } time.Sleep(1 * time.Second) } if err != nil { container.Terminate(ctx) return nil, nil, err } std, err := sql.Open("pgx", dsn) if err != nil { container.Terminate(ctx) return nil, nil, err } if err := goose.Up(std, migrationsDir); err != nil { container.Terminate(ctx) return nil, nil, err } std.Close() cleanup := func() { pool.Close() _ = container.Terminate(context.Background()) } return pool, cleanup, nil } type LomsIntegrationSuite struct { suite.Suite grpcSrv *grpc.Server grpcConn *grpc.ClientConn lomsClient pb.LOMSClient cleanup func() } func TestLomsIntegrationSuite(t *testing.T) { suite.RunSuite(t, new(LomsIntegrationSuite)) } func (s *LomsIntegrationSuite) BeforeAll(t provider.T) { ctx := context.Background() t.WithNewStep("init cart-service", func(sCtx provider.StepCtx) { pool, cleanup, err := startPostgres(ctx, migrationsDir) sCtx.Require().NoError(err, "failed postgres setup") s.cleanup = cleanup orderRepo := ordersRepository.NewOrderRepository(pool) stockRepo := stocksRepository.NewStockRepository(pool, pool) txManager := postgres.NewTxManager(pool, pool) svc := lomsService.NewLomsService(orderRepo, stockRepo, txManager, &mockKafkaProducer{}) lomsServer := server.NewServer(svc) lis, err := net.Listen("tcp", "127.0.0.1:0") sCtx.Require().NoError(err) s.grpcSrv = grpc.NewServer() pb.RegisterLOMSServer(s.grpcSrv, lomsServer) go func() { _ = s.grpcSrv.Serve(lis) }() time.Sleep(50 * time.Millisecond) conn, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) sCtx.Require().NoError(err) s.grpcConn = conn s.lomsClient = pb.NewLOMSClient(conn) }) } func (s *LomsIntegrationSuite) AfterAll(t provider.T) { s.grpcSrv.Stop() _ = s.grpcConn.Close() s.cleanup() } func (s *LomsIntegrationSuite) TestOrderProcessPositive(t provider.T) { ctx := context.Background() var orderID int64 t.WithNewStep("create order", func(sCtx provider.StepCtx) { req := &pb.OrderCreateRequest{ UserId: int64(testUID), Items: []*pb.OrderItem{{ Sku: int64(testSKU), Count: testCount, }}, } resp, err := s.lomsClient.OrderCreate(ctx, req) sCtx.Require().NoError(err) sCtx.Require().Greater(resp.OrderId, int64(0)) orderID = resp.OrderId }) t.WithNewStep("verify order info (NEW)", func(sCtx provider.StepCtx) { resp, err := s.lomsClient.OrderInfo(ctx, &pb.OrderInfoRequest{OrderId: orderID}) sCtx.Require().NoError(err) sCtx.Require().Equal("awaiting payment", resp.Status) sCtx.Require().Equal(int64(testUID), resp.UserId) sCtx.Require().Len(resp.Items, 1) sCtx.Require().Equal(int64(testSKU), resp.Items[0].Sku) sCtx.Require().Equal(testCount, resp.Items[0].Count) }) t.WithNewStep("pay order", func(sCtx provider.StepCtx) { _, err := s.lomsClient.OrderPay(ctx, &pb.OrderPayRequest{OrderId: orderID}) sCtx.Require().NoError(err) }) t.WithNewStep("verify order info (PAYED)", func(sCtx provider.StepCtx) { resp, err := s.lomsClient.OrderInfo(ctx, &pb.OrderInfoRequest{OrderId: orderID}) sCtx.Require().NoError(err) sCtx.Require().Equal("payed", resp.Status) }) } func (s *LomsIntegrationSuite) TestStocksInfoPositive(t provider.T) { ctx := context.Background() t.WithNewStep("call StocksInfo", func(sCtx provider.StepCtx) { resp, err := s.lomsClient.StocksInfo(ctx, &pb.StocksInfoRequest{Sku: int64(testSKU)}) sCtx.Require().NoError(err) sCtx.Require().Greater(resp.Count, uint32(0)) }) } func (s *LomsIntegrationSuite) TestOrderCreate_SuccessAsync(t provider.T) { t.Title("Успешное создание заказов (async)") const ( sku = 1625903 count = 1 ordersCount = 100 ) var ( userIDs = []int64{42, 43, 44} orders = make([]struct { userID int64 orderID int64 }, ordersCount) initStocksCount uint64 ctx = context.Background() ) t.WithNewStep("Получение изначальных стоков", func(sCtx provider.StepCtx) { stockCount, err := s.lomsClient.StocksInfo(ctx, &pb.StocksInfoRequest{Sku: sku}) sCtx.Require().NoError(err) initStocksCount = uint64(stockCount.GetCount()) }) t.WithNewStep("Создание заказов", func(sCtx provider.StepCtx) { var wg sync.WaitGroup for i := range ordersCount { wg.Add(1) go func() { defer wg.Done() userID := userIDs[i%len(userIDs)] req := &pb.OrderCreateRequest{ UserId: userID, Items: []*pb.OrderItem{ { Sku: sku, Count: count, }, }, } orderCreateResp, err := s.lomsClient.OrderCreate(ctx, req) sCtx.Require().NoError(err) sCtx.Require().Greater(orderCreateResp.OrderId, int64(0)) orders[i].userID = userID orders[i].orderID = orderCreateResp.OrderId }() } wg.Wait() }) t.WithNewStep("Проверка заказов", func(sCtx provider.StepCtx) { for _, order := range orders { res, err := s.lomsClient.OrderInfo(ctx, &pb.OrderInfoRequest{ OrderId: order.orderID, }) sCtx.Require().NoError(err) expected := entity.Order{ Status: "awaiting payment", UserID: entity.ID(order.userID), Items: []entity.OrderItem{ { ID: sku, Count: count, }, }, } sCtx.Require().Equal(expected.Status, res.Status, "Не совпадает статус заказа") sCtx.Require().Equal(expected.UserID, entity.ID(res.UserId), "Не совпадает пользователь заказа") sCtx.Require().Equal(len(expected.Items), len(res.Items), "Не совпадает количество товаров в заказе") } }) t.WithNewStep("Проверка стоков", func(sCtx provider.StepCtx) { stocksCount, err := s.lomsClient.StocksInfo(ctx, &pb.StocksInfoRequest{ Sku: sku, }) sCtx.Require().NoError(err) sCtx.Require().Equal(uint32(initStocksCount-ordersCount*count), stocksCount.Count) }) } func (s *LomsIntegrationSuite) TestOrderCreate_NoStockInfo(t provider.T) { t.Title("Неуспешное создание заказ из-за отсутствия информации о стоках товара") const ( userID = 42 sku = 404 ) ctx := context.Background() t.WithNewStep("Создание заказа", func(sCtx provider.StepCtx) { req := &pb.OrderCreateRequest{ UserId: userID, Items: []*pb.OrderItem{ { Sku: sku, Count: 1, }, }, } _, err := s.lomsClient.OrderCreate(ctx, req) e, ok := status.FromError(err) sCtx.Require().True(ok) sCtx.Require().Equal(codes.FailedPrecondition, e.Code(), "expect 400 (failed precondition) status code, got: %s", err.Error()) }) }