blob: 7c4232a5667aa0cfb486011e91f9c027adc9ed3b [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package tests
import (
"context"
"errors"
"fmt"
iggcon "github.com/apache/iggy/foreign/go/contracts"
"github.com/apache/iggy/foreign/go/iggycli"
"github.com/apache/iggy/foreign/go/tcp"
"github.com/cucumber/godog"
"github.com/google/uuid"
"os"
"testing"
)
type basicMessagingCtxKey struct{}
type basicMessagingCtx struct {
serverAddr *string
client iggycli.Client
lastSentMessage *iggcon.IggyMessage
lastPollMessages *iggcon.PolledMessage
lastStreamID *uint32
lastStreamName *string
lastTopicID *uint32
lastTopicName *string
lastTopicPartitions *uint32
}
func getBasicMessagingCtx(ctx context.Context) *basicMessagingCtx {
return ctx.Value(basicMessagingCtxKey{}).(*basicMessagingCtx)
}
func givenRunningServer(ctx context.Context) error {
c := getBasicMessagingCtx(ctx)
addr := os.Getenv("IGGY_TCP_ADDRESS")
if addr == "" {
addr = "127.0.0.1:8090"
}
c.serverAddr = &addr
return nil
}
func givenAuthenticationAsRoot(ctx context.Context) error {
c := getBasicMessagingCtx(ctx)
serverAddr := *c.serverAddr
client, err := iggycli.NewIggyClient(
iggycli.WithTcp(
tcp.WithServerAddress(serverAddr),
),
)
if err != nil {
return fmt.Errorf("error creating client: %w", err)
}
if err = client.Ping(); err != nil {
return fmt.Errorf("error pinging client: %w", err)
}
if _, err = client.LoginUser("iggy", "iggy"); err != nil {
return fmt.Errorf("error logging in: %v", err)
}
c.client = client
return nil
}
func whenSendMessages(
ctx context.Context,
messagesCount uint32,
streamID uint32,
topicID uint32,
partitionID uint32) error {
c := getBasicMessagingCtx(ctx)
messages, err := createTestMessages(messagesCount)
if err != nil {
return fmt.Errorf("error creating test messages: %w", err)
}
streamIdentifier, _ := iggcon.NewIdentifier(streamID)
topicIdentifier, _ := iggcon.NewIdentifier(topicID)
partitioning := iggcon.PartitionId(partitionID)
if err = c.client.SendMessages(streamIdentifier, topicIdentifier, partitioning, messages); err != nil {
return fmt.Errorf("failed to sending messages: %w", err)
}
c.lastSentMessage = &messages[len(messages)-1]
return nil
}
func createTestMessages(count uint32) ([]iggcon.IggyMessage, error) {
messages := make([]iggcon.IggyMessage, 0, count)
for i := 0; uint32(i) < count; i++ {
id := uuid.New()
payload := []byte(fmt.Sprintf("test message %d", i))
message, err := iggcon.NewIggyMessage(payload, iggcon.WithID(id))
if err != nil {
return nil, fmt.Errorf("failed to create message: %w", err)
}
messages = append(messages, message)
}
return messages, nil
}
func whenPollMessages(
ctx context.Context,
streamID uint32,
topicID uint32,
partitionID uint32,
startOffset uint64) error {
c := getBasicMessagingCtx(ctx)
consumer := iggcon.DefaultConsumer()
streamIdentifier, _ := iggcon.NewIdentifier(streamID)
topicIdentifier, _ := iggcon.NewIdentifier(topicID)
uint32PartitionID := partitionID
polledMessages, err := c.client.PollMessages(
streamIdentifier,
topicIdentifier,
consumer,
iggcon.OffsetPollingStrategy(startOffset),
100,
false,
&uint32PartitionID,
)
if err != nil {
return fmt.Errorf("failed to poll messages: %w", err)
}
c.lastPollMessages = polledMessages
return nil
}
func thenMessageSentSuccessfully(_ context.Context) error {
return nil
}
func thenShouldReceiveMessages(ctx context.Context, expectedCount uint32) error {
polledMessages := getBasicMessagingCtx(ctx).lastPollMessages
if uint32(len(polledMessages.Messages)) != expectedCount {
return fmt.Errorf("expected %d messages, but there is %d", expectedCount, len(polledMessages.Messages))
}
return nil
}
func thenMessagesHaveSequentialOffsets(
ctx context.Context,
startOffset uint64,
endOffset uint64) error {
polledMessages := getBasicMessagingCtx(ctx).lastPollMessages
for i, m := range polledMessages.Messages {
expectedOffset := startOffset + uint64(i)
if expectedOffset != m.Header.Offset {
return fmt.Errorf("message at index %d should have offset %d", i, expectedOffset)
}
}
lastMessage := polledMessages.Messages[len(polledMessages.Messages)-1]
if lastMessage.Header.Offset != endOffset {
return fmt.Errorf("last message should have offset %d", endOffset)
}
return nil
}
func thenMessagesHaveExpectedPayload(ctx context.Context) error {
polledMessages := getBasicMessagingCtx(ctx).lastPollMessages
for i, m := range polledMessages.Messages {
expectedPayload := fmt.Sprintf("test message %d", i)
if expectedPayload != string(m.Payload) {
return fmt.Errorf("message at offset %d should have payload '%s'", i, expectedPayload)
}
}
return nil
}
func thenLastPolledMessageMatchesSent(ctx context.Context) error {
c := getBasicMessagingCtx(ctx)
polledMessages := c.lastPollMessages
sentMessage := c.lastSentMessage
if len(polledMessages.Messages) == 0 {
return errors.New("should have at least one polled message")
}
lastPolled := polledMessages.Messages[len(polledMessages.Messages)-1]
if lastPolled.Header.Id != sentMessage.Header.Id {
return fmt.Errorf("message IDs should match: sent %d, polled %d", sentMessage.Header.Id, lastPolled.Header.Id)
}
if string(lastPolled.Payload) != string(sentMessage.Payload) {
return fmt.Errorf("message payload should match: sent %s, polled %s", sentMessage.Payload, lastPolled.Header.Id)
}
return nil
}
func givenNoStreams(ctx context.Context) error {
client := getBasicMessagingCtx(ctx).client
streams, err := client.GetStreams()
if err != nil {
return fmt.Errorf("failed to get streams: %w", err)
}
if len(streams) != 0 {
return errors.New("system should have no stream initially")
}
return err
}
func whenCreateStream(ctx context.Context, streamID uint32, streamName string) error {
c := getBasicMessagingCtx(ctx)
stream, err := c.client.CreateStream(streamName, &streamID)
if err != nil {
return fmt.Errorf("failed to create stream: %w", err)
}
c.lastStreamID = &stream.Id
c.lastStreamName = &stream.Name
return nil
}
func thenStreamCreatedSuccessfully(ctx context.Context) error {
if getBasicMessagingCtx(ctx).lastStreamID == nil {
return errors.New("stream should have been created")
}
return nil
}
func thenStreamHasIDAndName(
ctx context.Context,
expectedID uint32,
expectedName string) error {
c := getBasicMessagingCtx(ctx)
streamID := *c.lastStreamID
streamName := *c.lastStreamName
if streamID != expectedID {
return fmt.Errorf("expected stream ID %d, got %d", expectedID, streamID)
}
if streamName != expectedName {
return fmt.Errorf("expected stream name %s, got %s", expectedName, streamName)
}
return nil
}
func whenCreateTopic(
ctx context.Context,
topicID uint32,
topicName string,
streamID uint32,
partitionsCount uint32) error {
c := getBasicMessagingCtx(ctx)
streamIdentifier, _ := iggcon.NewIdentifier(streamID)
uint32TopicID := topicID
topic, err := c.client.CreateTopic(
streamIdentifier,
topicName,
partitionsCount,
iggcon.CompressionAlgorithmNone,
iggcon.IggyExpiryNeverExpire,
0,
nil,
&uint32TopicID,
)
if err != nil {
return fmt.Errorf("failed to create topic: %w", err)
}
c.lastTopicID = &topic.Id
c.lastTopicName = &topic.Name
c.lastTopicPartitions = &topic.PartitionsCount
return nil
}
func thenTopicCreatedSuccessfully(ctx context.Context) error {
if getBasicMessagingCtx(ctx).lastTopicID == nil {
return errors.New("topic should have been created")
}
return nil
}
func thenTopicHasIDAndName(
ctx context.Context,
expectedID uint32,
expectedName string) error {
c := getBasicMessagingCtx(ctx)
topicID := *c.lastTopicID
topicName := *c.lastTopicName
if topicID != expectedID {
return fmt.Errorf("expected topic ID %d, got %d", expectedID, topicID)
}
if topicName != expectedName {
return fmt.Errorf("expected topic name %s, got %s", expectedName, topicName)
}
return nil
}
func thenTopicsHasPartitions(ctx context.Context, expectedTopicPartitions uint32) error {
topicPartitions := *getBasicMessagingCtx(ctx).lastTopicPartitions
if topicPartitions != expectedTopicPartitions {
return errors.New("topic should have expected number of partitions")
}
return nil
}
func initScenarios(sc *godog.ScenarioContext) {
sc.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) {
return context.WithValue(context.Background(), basicMessagingCtxKey{}, &basicMessagingCtx{}), nil
})
sc.Step(`I have a running Iggy server`, givenRunningServer)
sc.Step(`I am authenticated as the root user`, givenAuthenticationAsRoot)
sc.Step(`^I send (\d+) messages to stream (\d+), topic (\d+), partition (\d+)$`, whenSendMessages)
sc.Step(`^I poll messages from stream (\d+), topic (\d+), partition (\d+) starting from offset (\d+)$`, whenPollMessages)
sc.Step(`all messages should be sent successfully`, thenMessageSentSuccessfully)
sc.Step(`^I should receive (\d+) messages$`, thenShouldReceiveMessages)
sc.Step(`^the messages should have sequential offsets from (\d+) to (\d+)$`, thenMessagesHaveSequentialOffsets)
sc.Step(`each message should have the expected payload content`, thenMessagesHaveExpectedPayload)
sc.Step(`the last polled message should match the last sent message`, thenLastPolledMessageMatchesSent)
sc.Step(`^the stream should have ID (\d+) and name (.+)$`, thenStreamHasIDAndName)
sc.Step(`the stream should be created successfully`, thenStreamCreatedSuccessfully)
sc.Step(`^I create a stream with ID (\d+) and name (.+)$`, whenCreateStream)
sc.Step(`I have no streams in the system`, givenNoStreams)
sc.Step(`^I create a topic with ID (\d+) and name (.+) in stream (\d+) with (\d+) partitions$`, whenCreateTopic)
sc.Step(`the topic should be created successfully`, thenTopicCreatedSuccessfully)
sc.Step(`^the topic should have ID (\d+) and name (.+)$`, thenTopicHasIDAndName)
sc.Step(`^the topic should have (\d+) partitions$`, thenTopicsHasPartitions)
}
func TestFeatures(t *testing.T) {
suite := godog.TestSuite{
ScenarioInitializer: initScenarios,
Options: &godog.Options{
Format: "pretty",
Paths: []string{"../../scenarios/basic_messaging.feature"},
TestingT: t,
},
}
if suite.Run() != 0 {
t.Fatal("failing feature tests")
}
}