| // 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") |
| } |
| } |