Repository: fppt/jedis-mock
Pull request #411 (2024-01-20 22:08:01)

Work for 1.1.0


Patch:
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index cd43e45..47a0413 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -2,9 +2,11 @@ name: build
 
 on:
   push:
-    branches: [master]
+    branches:
+      - master
   pull_request:
-    branches: [master]
+    branches:
+      - master
 
 jobs:
   build:
diff --git a/pom.xml b/pom.xml
index f62752a..8ccea66 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,11 +43,12 @@
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <junit.version>5.10.1</junit.version>
-        <assertj.version>3.24.2</assertj.version>
+        <assertj.version>3.25.1</assertj.version>
         <jedis.version>5.1.0</jedis.version>
         <testcontainers.version>1.19.3</testcontainers.version>
-        <spotbugs.version>4.8.2.0</spotbugs.version>
-        <slf4j.version>2.0.9</slf4j.version>
+        <spotbugs.version>4.8.3.0</spotbugs.version>
+        <slf4j.version>2.0.11</slf4j.version>
+        <java.version>1.8</java.version>
     </properties>
 
     <distributionManagement>
@@ -106,13 +107,13 @@
         <dependency>
             <groupId>io.lettuce</groupId>
             <artifactId>lettuce-core</artifactId>
-            <version>6.3.0.RELEASE</version>
+            <version>6.3.1.RELEASE</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-simple</artifactId>
-            <version>2.0.9</version>
+            <version>2.0.11</version>
             <scope>test</scope>
         </dependency>
         <!-- Used for creating an testcontainer of Redis for testing and comparing behaviour-->
@@ -137,15 +138,22 @@
         <dependency>
             <groupId>nl.jqno.equalsverifier</groupId>
             <artifactId>equalsverifier</artifactId>
-            <version>3.15.4</version>
+            <version>3.15.6</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.redisson</groupId>
             <artifactId>redisson</artifactId>
-            <version>3.25.0</version>
+            <version>3.26.0</version>
             <scope>test</scope>
         </dependency>
+
+        <!--    spotbugs annotations    -->
+        <dependency>
+            <groupId>com.github.spotbugs</groupId>
+            <artifactId>spotbugs-annotations</artifactId>
+            <version>4.7.2</version>
+        </dependency>
     </dependencies>
 
     <build>
@@ -167,16 +175,16 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
-                <version>3.11.0</version>
+                <version>3.12.1</version>
                 <configuration>
-                    <source>1.8</source>
-                    <target>1.8</target>
+                    <target>${java.version}</target>
+                    <source>${java.version}</source>
                 </configuration>
             </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
-                <version>3.2.2</version>
+                <version>3.2.5</version>
                 <configuration>
                     <useFile>true</useFile>
                     <redirectTestOutputToFile>true</redirectTestOutputToFile>
@@ -205,6 +213,15 @@
                         </goals>
                     </execution>
                 </executions>
+                <configuration>
+                    <tags>
+                        <tag>
+                            <name>asymptotic</name>
+                            <placement>a</placement>
+                            <head>Asymptotic:</head>
+                        </tag>
+                    </tags>
+                </configuration>
             </plugin>
             <plugin>
                 <groupId>com.github.spotbugs</groupId>
diff --git a/src/main/java/com/github/fppt/jedismock/datastructures/streams/RMStream.java b/src/main/java/com/github/fppt/jedismock/datastructures/streams/RMStream.java
new file mode 100644
index 0000000..ea91cc8
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/datastructures/streams/RMStream.java
@@ -0,0 +1,91 @@
+package com.github.fppt.jedismock.datastructures.streams;
+
+import com.github.fppt.jedismock.datastructures.RMDataStructure;
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.exception.WrongStreamKeyException;
+import com.github.fppt.jedismock.exception.WrongValueTypeException;
+
+import static java.lang.Long.compareUnsigned;
+import static java.lang.Long.parseUnsignedLong;
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.TOP_ERROR;
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.ID_OVERFLOW_ERROR;
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.INVALID_ID_ERROR;
+import static java.lang.Long.toUnsignedString;
+
+public class RMStream implements RMDataStructure {
+    private final SequencedMap<StreamId, SequencedMap<Slice, Slice>> storedData;
+    private StreamId lastId;
+
+
+    public RMStream() {
+        storedData = new SequencedMap<>();
+        lastId = new StreamId();
+    }
+
+    public StreamId getLastId() {
+        return lastId;
+    }
+
+    public SequencedMap<StreamId, SequencedMap<Slice, Slice>> getStoredData() {
+        return storedData;
+    }
+
+    public void updateLastId(StreamId id) {
+        lastId = id;
+    }
+
+    public Slice replaceAsterisk(Slice key) throws WrongStreamKeyException {
+        if (key.toString().equals("*")) {
+            /* 0xFFFFFFFFFFFFFFFF-0xFFFFFFFFFFFFFFFF is already in use - overflow */
+            if (lastId.getFirstPart() == -1 && lastId.getSecondPart() == -1) {
+                throw new WrongStreamKeyException(ID_OVERFLOW_ERROR);
+            }
+
+            long secondPart = lastId.getSecondPart() + 1;
+            long firstPart = lastId.getFirstPart() + (lastId.getSecondPart() == -1 ? 1 : 0);
+
+            return Slice.create(toUnsignedString(firstPart) + "-" + toUnsignedString(secondPart));
+        }
+
+        String[] parsedKey = key.toString().split("-");
+
+        if (parsedKey.length != 2) {
+            return key; /* Wrong key format - will be caught when creating StreamId */
+        }
+
+        try {
+            if (compareUnsigned(lastId.getFirstPart(), parseUnsignedLong(parsedKey[0])) > 0) {
+                throw new WrongStreamKeyException(TOP_ERROR);
+            }
+
+            if (parsedKey[1].equals("*")) {
+                if (compareUnsigned(lastId.getFirstPart(), parseUnsignedLong(parsedKey[0])) == 0) {
+                    /* The second part is 0xFFFFFFFFFFFFFFFF - overflow */
+                    if (lastId.getSecondPart() == -1) {
+                        throw new WrongStreamKeyException(TOP_ERROR);
+                    }
+
+                    /* Incrementing the second part */
+                    return Slice.create(parsedKey[0] + "-" + toUnsignedString(lastId.getSecondPart() + 1));
+                }
+
+                /* Use 0 as the smallest unsigned long number */
+                return Slice.create(parsedKey[0] + "-0");
+            }
+        } catch (NumberFormatException e) {
+            throw new WrongStreamKeyException(INVALID_ID_ERROR);
+        }
+
+        return key;
+    }
+
+    @Override
+    public void raiseTypeCastException() {
+        throw new WrongValueTypeException("WRONGTYPE RMStream value is used in the wrong place");
+    }
+
+    @Override
+    public String getTypeName() {
+        return "stream";
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/datastructures/streams/SequencedMap.java b/src/main/java/com/github/fppt/jedismock/datastructures/streams/SequencedMap.java
new file mode 100644
index 0000000..2224310
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/datastructures/streams/SequencedMap.java
@@ -0,0 +1,344 @@
+package com.github.fppt.jedismock.datastructures.streams;
+
+import java.util.AbstractMap;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+/**
+ * An associative array with O(1) get, delete operations.<br>
+ * Can be interpreted as a sequence of nodes that allows to iterate map.
+ *
+ * @param <K> keys type, must implement {@link java.lang.Comparable Comparable}
+ * @param <V> values type
+ */
+public class SequencedMap<K extends Comparable<K>, V> implements Iterable<Map.Entry<K, V>> {
+    /**
+     * A node that replaces value in {@code HashMap}. Contains additional data of next and previous nodes.
+     */
+    protected class LinkedMapNode {
+        protected final V value;
+        protected K next;
+        protected K prev;
+
+        LinkedMapNode(V value) {
+            this.value = value;
+        }
+
+        public LinkedMapNode setNext(K next) {
+            this.next = next;
+            return this;
+        }
+
+        public LinkedMapNode setPrev(K prev) {
+            this.prev = prev;
+            return this;
+        }
+    }
+
+    private final Map<K, LinkedMapNode> map;
+    private K tail;
+    private K head;
+    private int size;
+
+    public SequencedMap() {
+        this.map = new HashMap<>();
+        size = 0;
+    }
+
+    /**
+     * Add a mapping to the end of {@code SequencedMap}
+     * @asymptotic O(1)
+     * @param key the key with which the specified value is to be associated
+     * @param value the value to be associated with the specified key
+     */
+    public void append(K key, V value) {
+        if (size == 0) {
+            head = key; // the map is empty, so the first appended element becomes the head
+        } else {
+            map.get(tail).setNext(key); // the map is not empty, so we have to update the reference
+        }
+
+        map.put(key, new LinkedMapNode(value).setPrev(tail));
+
+
+        ++size;
+        tail = key;
+    }
+
+    /**
+     * Remove the mapping for the given key from map if it exists
+     * @asymptotic O(1) regardless the size of map
+     * @param key the key of mapping to be removed
+     * @return deleted entry if a mapping for the key exists otherwise {@code null}
+     */
+    public Map.Entry<K, V> remove(K key) {
+        if (!map.containsKey(key)) {
+            return null;
+        }
+
+        if (size == 1) {
+            size = 0;
+            tail = null;
+            head = null;
+
+            return new AbstractMap.SimpleEntry<>(key, map.remove(key).value);
+        }
+
+        if (key.equals(tail)) {
+            tail = getPreviousKey(key);
+            map.get(tail).next = null;
+        } else if (key.equals(head)) {
+            head = getNextKey(key);
+            map.get(head).prev = null;
+        } else {
+            setNextKey(getPreviousKey(key), getNextKey(key));
+            setPreviousKey(getNextKey(key), getPreviousKey(key));
+        }
+
+        --size;
+        return new AbstractMap.SimpleEntry<>(key, map.remove(key).value);
+    }
+
+    /**
+     * Remove the mapping for the first key from map if it exists
+     * @asymptotic O(1) regardless the size of map
+     */
+    public void removeHead() {
+        if (size == 0) {
+            return;
+        }
+
+        --size;
+        head = getNextKey(head);
+    }
+
+    /**
+     * Get the value to which the given key is mapped
+     * @asymptotic O(1)
+     * @param key the key whose associated value is to be returned
+     * @return the value to which the given key is mapped or {@code null} if map does not contain it
+     */
+    public V get(K key) {
+        LinkedMapNode node = map.get(key);
+
+        if (node == null) {
+            return null;
+        }
+
+        return node.value;
+    }
+
+    /**
+     * Get the size of map
+     * @return existing mappings count
+     */
+    public int size() {
+        return size;
+    }
+
+    /**
+     * Get the key of the next node. If there is no mapping for the provided key {@code NullPointerException} is thrown.
+     * <b>Private API:</b> is accessible only to {@code SequencedMapIterator}
+     *
+     * @param key the key of the node whose following key is being searched for
+     * @return the key of the node that follows the given one
+     */
+    K getNextKey(K key) {
+        return map.get(key).next;
+    }
+
+    /**
+     * Set the key of the next node. If there is no mapping for the provided key {@code NullPointerException} is thrown.
+     * <b>Private API:</b> is accessible only to {@code SequencedMapIterator}
+     *
+     * @param key the key of the node whose following key is being updated
+     * @param next the key of the node which is to follow the given one
+     */
+    void setNextKey(K key, K next) {
+        if (!map.containsKey(next) || !map.containsKey(key)) {
+            throw new NullPointerException("Map does not contain some of provided keys");
+        }
+
+        map.get(key).next = next;
+    }
+
+    /**
+     * Get the key of the previous node. If there is no mapping for the key {@code NullPointerException} is thrown.
+     * <b>Private API:</b> is accessible only to {@code SequencedMapIterator}
+     *
+     * @param key the key of the node whose previous key is being searched for
+     * @return the key of the node that precedes the given one
+     */
+    public K getPreviousKey(K key) {
+        return map.get(key).prev;
+    }
+
+    /**
+     * Set the key of the previous node. If there is no mapping for the provided key {@code NullPointerException} is thrown.
+     * <b>Private API:</b> is accessible only to {@code SequencedMapIterator}
+     *
+     * @param key the key of the node whose previous key is being updated
+     * @param prev the key of the node which is to precede the given one
+     */
+    public void setPreviousKey(K key, K prev) {
+        if (!map.containsKey(prev) || !map.containsKey(key)) {
+            throw new NullPointerException("Map does not contain some of provided keys");
+        }
+
+        map.get(key).prev = prev;
+    }
+
+    /**
+     * Checks whether a mapping for the given key exists.
+     *
+     * @return {@code true} if the mapping exists otherwise {@code false}
+     */
+    public boolean contains(K key) {
+        return map.containsKey(key);
+    }
+
+    /**
+     * Get the key of the first mapping.
+     *
+     * @return the first node key in the sequence
+     */
+    public K getHead() {
+        return head;
+    }
+
+    /**
+     * Get the key of the last mapping.
+     *
+     * @return the last node key in the sequence
+     */
+    public K getTail() {
+        return tail;
+    }
+
+    /**
+     * Get {@link SequencedMapForwardIterator SequencedMapIterator}
+     * whose iteration starts from the head node
+     *
+     * @return iterator that allows to iterate map
+     */
+    @Override
+    public SequencedMapForwardIterator<K, V> iterator() {
+        return new SequencedMapForwardIterator<>(head, this);
+    }
+
+    /**
+     * Get {@link SequencedMapForwardIterator SequencedMapForwardIterator}
+     * whose iteration starts from the provided key if there is a mapping for it otherwise
+     * iteration starts from the closest higher element.
+     * If {@code key} is {@code null} than {@code NullPointerException} is thrown.
+     *
+     * @param key the key which is the start of iteration
+     * @return iterator that points to the provided key or the closest higher key
+     */
+    public SequencedMapForwardIterator<K, V> iterator(K key) {
+        if (key == null) {
+            throw new NullPointerException("Key is null");
+        }
+
+        if (map.containsKey(key)) {
+            return new SequencedMapForwardIterator<>(key, this);
+        }
+
+        // failed to get -> searching the first suitable element (potentially can be replaced with binary search)
+        K startKey = null;
+        SequencedMapForwardIterator<K, V> it = new SequencedMapForwardIterator<>(null, this);
+
+        if (key != head) { // searching the first node
+            while (it.hasNext()) {
+                startKey = it.next().getKey();
+
+                if (startKey.compareTo(key) >= 0) {
+                    break;
+                }
+            }
+        }
+
+        if (startKey != null) { // map might be empty
+            it.stepBack();
+        }
+
+        return it;
+    }
+
+    /**
+     * Get {@link SequencedMapReverseIterator SequencedMapReverseIterator}
+     * whose iteration starts from the tail
+     *
+     * @return iterator that allows to iterate map in reversed order
+     */
+    public SequencedMapReverseIterator<K, V> reverseIterator() {
+        return new SequencedMapReverseIterator<>(tail, this);
+    }
+
+    /**
+     * Get {@link SequencedMapReverseIterator SequencedMapReverseIterator}
+     * whose iteration starts from the provided key if there is a mapping for it otherwise
+     * iteration starts from the closest lower element.
+     * If {@code key} is {@code null} than {@code NullPointerException} is thrown.
+     *
+     * @param key the key which is the start of iteration
+     * @return iterator that points to the provided key or the closest lower key
+     */
+    public SequencedMapReverseIterator<K, V> reverseIterator(K key) {
+        if (key == null) {
+            throw new NullPointerException("Key is null");
+        }
+
+        if (map.containsKey(key)) {
+            return new SequencedMapReverseIterator<>(key, this);
+        }
+
+        // failed to get -> searching the first suitable element (potentially can be replaced with binary search)
+        K startKey = null;
+        SequencedMapReverseIterator<K, V> it = new SequencedMapReverseIterator<>(null, this);
+
+        if (key != tail) { // searching the first node
+            while (it.hasNext()) {
+                startKey = it.next().getKey();
+
+                if (startKey.compareTo(key) <= 0) {
+                    break;
+                }
+            }
+        }
+
+        if (startKey != null) { // map might be empty
+            it.stepBack();
+        }
+
+        return it;
+    }
+
+    /**
+     * Performs the given action for each element of the map.<br>
+     * Method's behaviour:
+     * <pre>{@code
+     * for (Map.entry<K, V> entry: map) {
+     *      action.accept(entry.getKey(), entry.getValue());
+     * }
+     * }</pre>
+     * @param action function to be executed for each element of the map
+     */
+    public void forEach(BiConsumer<? super K, ? super V> action) {
+        if (action == null) {
+            throw new NullPointerException();
+        }
+
+        if (size == 0) {
+            return;
+        }
+
+        K currKey = head;
+
+        do {
+            action.accept(currKey, map.get(currKey).value);
+            currKey = map.get(currKey).next;
+        } while (currKey != null);
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapForwardIterator.java b/src/main/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapForwardIterator.java
new file mode 100644
index 0000000..2d578df
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapForwardIterator.java
@@ -0,0 +1,70 @@
+package com.github.fppt.jedismock.datastructures.streams;
+
+import java.util.AbstractMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+/**
+ * Iterator for {@link SequencedMap SequencedMap}
+ *
+ * @param <K> keys type, must implement {@link java.lang.Comparable Comparable}
+ * @param <V> values type
+ */
+public class SequencedMapForwardIterator<K extends Comparable<K>, V> implements SequencedMapIterator<K, V> {
+    /**
+     * Iterator takes place before this key. If is {@code null} then iterator takes place before the head of the map.
+     */
+    private K curr;
+
+    /**
+     * Map that iterator refers to
+     */
+    private final SequencedMap<K, V> map;
+
+    public SequencedMapForwardIterator(K curr, SequencedMap<K, V> map) {
+        this.map = map;
+        this.curr = curr == null ? null : map.getPreviousKey(curr); // null is possible when map.size == 0
+    }
+
+    @Override
+    public boolean hasNext() {
+        if (curr == null) {
+            return map.getHead() != null;
+        }
+
+        return map.getNextKey(curr) != null;
+    }
+
+    @Override
+    public Map.Entry<K, V> next() {
+        if (!hasNext()) {
+            throw new NoSuchElementException("There is no elements left");
+        }
+
+        if (curr == null) {
+            curr = map.getHead();
+        } else {
+            curr = map.getNextKey(curr);
+        }
+
+        return new AbstractMap.SimpleEntry<>(curr, map.get(curr));
+    }
+
+    void stepBack() {
+        curr = map.getPreviousKey(curr);
+    }
+
+    /**
+     *
+     */
+    @Override
+    public void remove() {
+        if (curr == null) {
+            throw new IllegalStateException("'next' method has not been invoked yet");
+        } else {
+            K tmp = map.getPreviousKey(curr);
+            map.remove(curr);
+            curr = tmp;
+        }
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapIterator.java b/src/main/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapIterator.java
new file mode 100644
index 0000000..43907a8
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapIterator.java
@@ -0,0 +1,9 @@
+package com.github.fppt.jedismock.datastructures.streams;
+
+import java.util.Iterator;
+import java.util.Map;
+
+public interface SequencedMapIterator<K extends Comparable<K>, V> extends Iterator<Map.Entry<K, V>> {
+    @Override
+    Map.Entry<K, V> next();
+}
diff --git a/src/main/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapReverseIterator.java b/src/main/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapReverseIterator.java
new file mode 100644
index 0000000..e4eeedb
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapReverseIterator.java
@@ -0,0 +1,56 @@
+package com.github.fppt.jedismock.datastructures.streams;
+
+import java.util.AbstractMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+/**
+ * Reverse iterator for {@link SequencedMap SequencedMap}
+ *
+ * @param <K> keys type, must implement {@link java.lang.Comparable Comparable}
+ * @param <V> values type
+ */
+public class SequencedMapReverseIterator<K extends Comparable<K>, V> implements SequencedMapIterator<K, V> {
+    /**
+     * Iterator takes place after this key. If is {@code null} then iterator takes place after the tail of the map.
+     */
+    private K curr;
+
+    /**
+     * Map that iterator refers to
+     */
+    private final SequencedMap<K, V> map;
+
+    public SequencedMapReverseIterator(K curr, SequencedMap<K, V> map) {
+        this.map = map;
+        this.curr = curr == null ? null : map.getNextKey(curr); // null is possible when map.size == 0
+    }
+
+    @Override
+    public boolean hasNext() {
+        if (curr == null) {
+            return map.getTail() != null;
+        }
+
+        return map.getPreviousKey(curr) != null;
+    }
+
+    @Override
+    public Map.Entry<K, V> next() {
+        if (!hasNext()) {
+            throw new NoSuchElementException("There is no elements left");
+        }
+
+        if (curr == null) {
+            curr = map.getTail();
+        } else {
+            curr = map.getPreviousKey(curr);
+        }
+
+        return new AbstractMap.SimpleEntry<>(curr, map.get(curr));
+    }
+
+    void stepBack() {
+        curr = map.getNextKey(curr);
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/datastructures/streams/StreamErrors.java b/src/main/java/com/github/fppt/jedismock/datastructures/streams/StreamErrors.java
new file mode 100644
index 0000000..07444a4
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/datastructures/streams/StreamErrors.java
@@ -0,0 +1,15 @@
+package com.github.fppt.jedismock.datastructures.streams;
+
+public class StreamErrors {
+    public static final String ID_OVERFLOW_ERROR = "ERR The stream has exhausted the last possible ID, unable to add more items";
+    public static final String TOP_ERROR = "ERR The ID specified in XADD is equal or smaller than the target stream top item";
+    public static final String INVALID_ID_ERROR = "ERR Invalid stream ID specified as stream command argument";
+    public static final String ZERO_ERROR = "ERR The ID specified in XADD must be greater than 0-0";
+    public static final String SYNTAX_ERROR = "ERR syntax error";
+    public static final String NOT_AN_INTEGER_ERROR = "ERR value is not an integer or out of range";
+    public static final String LIMIT_OPTION_ERROR = "ERR syntax error, LIMIT cannot be used without the special ~ option";
+    public static final String XREAD_ARGS_ERROR = "ERR Unbalanced 'xread' list of streams: for each stream key an ID or '$' must be specified";
+    public static final String NEGATIVE_TIMEOUT_ERROR = "ERR timeout is negative";
+    public static final String RANGES_START_ID_ERROR = "ERR invalid start ID for the interval";
+    public static final String RANGES_END_ID_ERROR = "ERR invalid end ID for the interval";
+}
diff --git a/src/main/java/com/github/fppt/jedismock/datastructures/streams/StreamId.java b/src/main/java/com/github/fppt/jedismock/datastructures/streams/StreamId.java
new file mode 100644
index 0000000..e3c99bf
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/datastructures/streams/StreamId.java
@@ -0,0 +1,122 @@
+package com.github.fppt.jedismock.datastructures.streams;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.exception.WrongStreamKeyException;
+
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.RANGES_END_ID_ERROR;
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.RANGES_START_ID_ERROR;
+import static java.lang.Long.compareUnsigned;
+import static java.lang.Long.parseUnsignedLong;
+import static java.lang.Long.toUnsignedString;
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.INVALID_ID_ERROR;
+
+public final class StreamId implements Comparable<StreamId> {
+    private final long firstPart;
+    private final long secondPart;
+
+    public long getFirstPart() {
+        return firstPart;
+    }
+
+    public long getSecondPart() {
+        return secondPart;
+    }
+
+    /* 0-0 ID for PRIVATE API usage */
+    StreamId() {
+        this(0, 0);
+    }
+
+    /* ID from long numbers for PRIVATE API usage */
+    public StreamId(long firstPart, long secondPart) {
+        this.firstPart = firstPart;
+        this.secondPart = secondPart;
+    }
+
+    public StreamId(String key) throws WrongStreamKeyException {
+        Matcher matcher = Pattern.compile("(\\d+)(?:-(\\d+))?").matcher(key);
+        if (matcher.matches()) {
+            try {
+                firstPart = parseUnsignedLong(matcher.group(1));
+                secondPart = matcher.group(2) == null ? 0 : parseUnsignedLong(matcher.group(2));
+            } catch (NumberFormatException e) {
+                throw new WrongStreamKeyException(INVALID_ID_ERROR);
+            }
+        } else {
+            throw new WrongStreamKeyException(INVALID_ID_ERROR);
+        }
+    }
+
+    public StreamId(Slice slice) throws WrongStreamKeyException {
+        this(slice.toString());
+    }
+
+    public boolean isZero() {
+        return secondPart == 0 && firstPart == 0;
+    }
+
+    public StreamId increment() throws WrongStreamKeyException {
+        long second = secondPart + 1;
+        long first = firstPart;
+
+        if (compareUnsigned(second, 0) == 0) { // the previous one was 0xFFFFFFFFFFFFFFFF => update the first part
+            if (compareUnsigned(first, -1) == 0) {
+                throw new WrongStreamKeyException(RANGES_START_ID_ERROR);
+            }
+
+            ++first;
+        }
+
+        return new StreamId(first, second);
+    }
+
+    public StreamId decrement() throws WrongStreamKeyException {
+        long second = secondPart - 1;
+        long first = firstPart;
+
+        if (compareUnsigned(second, -1) == 0) { // the previous one was 0x0000000000000000 => update the first part
+            if (compareUnsigned(first, 0) == 0) {
+                throw new WrongStreamKeyException(RANGES_END_ID_ERROR);
+            }
+
+            --first;
+        }
+
+        return new StreamId(first, second);
+    }
+
+    @Override
+    public int compareTo(StreamId other) {
+        int firstPartComparison = compareUnsigned(firstPart, other.firstPart);
+        return firstPartComparison != 0
+                ? firstPartComparison
+                : compareUnsigned(secondPart, other.secondPart);
+    }
+
+    public Slice toSlice() {
+        return Slice.create(toString());
+    }
+
+    @Override
+    public String toString() {
+        return toUnsignedString(firstPart) + "-" + toUnsignedString(secondPart);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof StreamId)) return false;
+        StreamId streamId = (StreamId) o;
+        if (firstPart != streamId.firstPart) return false;
+        return secondPart == streamId.secondPart;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(firstPart, secondPart);
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/exception/WrongStreamKeyException.java b/src/main/java/com/github/fppt/jedismock/exception/WrongStreamKeyException.java
new file mode 100644
index 0000000..725ea95
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/exception/WrongStreamKeyException.java
@@ -0,0 +1,7 @@
+package com.github.fppt.jedismock.exception;
+
+public class WrongStreamKeyException extends Exception {
+    public WrongStreamKeyException(String message) {
+        super(message);
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/AbstractBPop.java b/src/main/java/com/github/fppt/jedismock/operations/AbstractBPop.java
index 02b63f4..fd62a77 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/AbstractBPop.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/AbstractBPop.java
@@ -25,11 +25,13 @@ public abstract class AbstractBPop extends AbstractRedisOperation {
         this.isInTransaction = state.isTransactionModeOn();
     }
 
+    @Override
+    protected int minArgs(){
+        return 2;
+    }
+
     @Override
     protected void doOptionalWork() {
-        if (params().size() < 2) {
-            throw new IndexOutOfBoundsException("require at least 2 params");
-        }
         timeoutNanos = toNanoTimeout(params().get(params().size() - 1).toString());
         keys = params().subList(0, params().size() - 1);
     }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/AbstractRedisOperation.java b/src/main/java/com/github/fppt/jedismock/operations/AbstractRedisOperation.java
index 3aae91f..9aaf677 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/AbstractRedisOperation.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/AbstractRedisOperation.java
@@ -1,12 +1,15 @@
 package com.github.fppt.jedismock.operations;
 
 import com.github.fppt.jedismock.datastructures.RMList;
+import com.github.fppt.jedismock.datastructures.streams.RMStream;
 import com.github.fppt.jedismock.datastructures.RMZSet;
 import com.github.fppt.jedismock.datastructures.RMSet;
 import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.server.Response;
 import com.github.fppt.jedismock.storage.RedisBase;
 
 import java.util.List;
+import java.util.Objects;
 
 public abstract class AbstractRedisOperation implements RedisOperation {
     private final RedisBase base;
@@ -17,13 +20,13 @@ public abstract class AbstractRedisOperation implements RedisOperation {
         this.params = params;
     }
 
-    protected void doOptionalWork(){
+    protected void doOptionalWork() {
         //Place Holder For Ops which need to so some operational work
     }
 
     protected abstract Slice response();
 
-    protected RedisBase base(){
+    protected RedisBase base() {
         return base;
     }
 
@@ -33,7 +36,7 @@ public abstract class AbstractRedisOperation implements RedisOperation {
 
     public RMList getListFromBaseOrCreateEmpty(Slice key) {
         RMList data = base().getList(key);
-        if(data == null) {
+        if (data == null) {
             return new RMList();
         }
 
@@ -42,28 +45,53 @@ public abstract class AbstractRedisOperation implements RedisOperation {
 
     public RMSet getSetFromBaseOrCreateEmpty(Slice key) {
         RMSet data = base().getSet(key);
-        if(data == null) {
+        if (data == null) {
             return new RMSet();
         }
 
         return data;
     }
 
+    public RMStream getStreamFromBaseOrCreateEmpty(Slice key) {
+        RMStream data = base().getStream(key);
+        return data == null ? new RMStream() : data;
+    }
+
     public RMZSet getZSetFromBaseOrCreateEmpty(Slice key) {
         RMZSet data = base().getZSet(key);
-        if(data == null) {
+        if (data == null) {
             return new RMZSet();
         }
         return data;
     }
 
+    /***
+     * Minimum number of arguments (inclusive)
+     */
+    protected int minArgs() {
+        return 0;
+    }
+
+    /***
+     * Maximum number of arguments (inclusive)
+     */
+    protected int maxArgs() {
+        return Integer.MAX_VALUE;
+    }
+
     @Override
-    public Slice execute(){
+    public Slice execute() {
+        RedisCommand self = Objects.requireNonNull(getClass().getAnnotation(RedisCommand.class));
         try {
+            //Validate the number of arguments
+            if (params().size() < minArgs() || params().size() > maxArgs()) {
+                return Response.error(String.format("ERR wrong number of arguments for '%s' command", self.value()));
+            }
             doOptionalWork();
             return response();
-        } catch (IndexOutOfBoundsException e){
-            throw new IllegalArgumentException("Recieved wrong number of arguments when executing command [" + getClass().getSimpleName() + "]", e);
+        } catch (IndexOutOfBoundsException e) {
+            throw new IllegalArgumentException(
+                    String.format("Received wrong number of arguments when executing command [%s]", self.value()), e);
         }
     }
 }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/connection/Echo.java b/src/main/java/com/github/fppt/jedismock/operations/connection/Echo.java
index 1886110..5dba132 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/connection/Echo.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/connection/Echo.java
@@ -14,10 +14,17 @@ class Echo extends AbstractRedisOperation {
         super(base, params);
     }
 
+    @Override
+    protected int minArgs() {
+        return 1;
+    }
+
+    @Override
+    protected int maxArgs() {
+        return 1;
+    }
+
     protected Slice response() {
-        if (params().size() != 1) {
-            return Response.error("ERR wrong number of arguments for 'echo' command");
-        }
         return Response.bulkString(params().get(0));
     }
 }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/hashes/HDel.java b/src/main/java/com/github/fppt/jedismock/operations/hashes/HDel.java
index 134a9f8..fa762a1 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/hashes/HDel.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/hashes/HDel.java
@@ -1,6 +1,5 @@
 package com.github.fppt.jedismock.operations.hashes;
 
-import com.github.fppt.jedismock.exception.WrongValueTypeException;
 import com.github.fppt.jedismock.operations.AbstractRedisOperation;
 import com.github.fppt.jedismock.operations.RedisCommand;
 import com.github.fppt.jedismock.server.Response;
@@ -15,14 +14,15 @@ class HDel extends AbstractRedisOperation {
         super(base, params);
     }
 
+    @Override
+    protected int minArgs() {
+        return 2;
+    }
+
     protected Slice response(){
         Slice key = params().get(0);
         int count = 0;
 
-        if (params().size() < 2) {
-            throw new WrongValueTypeException("ERR wrong number of arguments for 'hdel' command");
-        }
-
         for (int i = 1; i < params().size(); ++i) {
             Slice currKey = params().get(i);
             Slice oldValue = base().getSlice(key, currKey);
diff --git a/src/main/java/com/github/fppt/jedismock/operations/scripting/Eval.java b/src/main/java/com/github/fppt/jedismock/operations/scripting/Eval.java
index aa6be34..5f6e2d7 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/scripting/Eval.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/scripting/Eval.java
@@ -27,7 +27,6 @@ import static com.github.fppt.jedismock.operations.scripting.Script.getScriptSHA
 @RedisCommand("eval")
 public class Eval extends AbstractRedisOperation {
 
-    private static final String SCRIPT_PARAM_ERROR = "Wrong number of arguments for EVAL";
     private static final String SCRIPT_RUNTIME_ERROR = "Error running script (call to function returned nil)";
     private static final String REDIS_LUA = loadResource();
     private final Globals globals = JsePlatform.standardGlobals();
@@ -39,11 +38,12 @@ public class Eval extends AbstractRedisOperation {
     }
 
     @Override
-    public Slice response() {
-        if (params().size() < 2) {
-            return Response.error(SCRIPT_PARAM_ERROR);
-        }
+    protected int minArgs() {
+        return 2;
+    }
 
+    @Override
+    public Slice response() {
         final String script = params().get(0).toString();
 
         this.base().addCachedLuaScript(getScriptSHA(script), script);
diff --git a/src/main/java/com/github/fppt/jedismock/operations/scripting/EvalSha.java b/src/main/java/com/github/fppt/jedismock/operations/scripting/EvalSha.java
index 101f9d5..7daf21d 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/scripting/EvalSha.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/scripting/EvalSha.java
@@ -11,8 +11,6 @@ import java.util.List;
 
 @RedisCommand("evalsha")
 public class EvalSha extends AbstractRedisOperation {
-    private static final String SCRIPT_PARAM_ERROR = "Wrong number of arguments for EVALSHA";
-
     private final OperationExecutorState state;
 
     public EvalSha(final RedisBase base, final List<Slice> params, OperationExecutorState state) {
@@ -20,11 +18,13 @@ public class EvalSha extends AbstractRedisOperation {
         this.state = state;
     }
 
+    @Override
+    protected int minArgs() {
+        return 2;
+    }
+
     @Override
     protected Slice response() {
-        if (params().size() < 2) {
-            return Response.error(SCRIPT_PARAM_ERROR);
-        }
         final String sha = params().get(0).toString();
         final String script = base().getCachedLuaScript(sha);
         if (script == null) {
diff --git a/src/main/java/com/github/fppt/jedismock/operations/server/MockExecutor.java b/src/main/java/com/github/fppt/jedismock/operations/server/MockExecutor.java
index 37c461f..693e4b7 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/server/MockExecutor.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/server/MockExecutor.java
@@ -36,10 +36,10 @@ public class MockExecutor {
                 if (operation != null) {
                     if (state.isTransactionModeOn()) {
                         state.tx().add(operation);
+                        return Response.clientResponse(name, Response.QUEUED);
                     } else {
                         return Response.clientResponse(name, operation.execute());
                     }
-                    return Response.clientResponse(name, Response.OK);
                 } else {
                     return Response.error(String.format("Unsupported operation: %s", name));
                 }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sets/SMIsMember.java b/src/main/java/com/github/fppt/jedismock/operations/sets/SMIsMember.java
index 40b1e23..cdc0ffd 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/sets/SMIsMember.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/sets/SMIsMember.java
@@ -19,11 +19,12 @@ public class SMIsMember extends AbstractRedisOperation {
     }
 
     @Override
-    protected Slice response() {
-        if (params().size() < 2) {
-            return Response.error("ERR wrong number of arguments for 'smismember' command");
-        }
+    protected int minArgs() {
+        return 2;
+    }
 
+    @Override
+    protected Slice response() {
         Slice key = params().get(0);
         Stream<Slice> paramStream = params().subList(1, params().size()).stream();
         RMSet setDBObj = getSetFromBaseOrCreateEmpty(key);
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sets/SRandMember.java b/src/main/java/com/github/fppt/jedismock/operations/sets/SRandMember.java
index f237f04..d17bf86 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/sets/SRandMember.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/sets/SRandMember.java
@@ -23,8 +23,11 @@ public class SRandMember extends AbstractRedisOperation {
 
     @Override
     protected Slice response() {
-        Slice key = params().get(0);
-        RMSet set = base().getSet(key);
+        if (params().isEmpty()) {
+            return Response.error("ERR wrong number of arguments for 'srandmember' command");
+        }
+
+        RMSet set = base().getSet(params().get(0));
         int number;
         if (params().size() > 1) {
             if (set == null) {
@@ -38,8 +41,8 @@ public class SRandMember extends AbstractRedisOperation {
             number = 1;
         }
 
-        //TODO: more effective algorithms should be used here,
-        //avoiding conversion of set to list, shuffling all the elements etc.
+        // TODO: more effective algorithms should be used here,
+        // avoiding conversion of set to list, shuffling all the elements etc.
         List<Slice> list = new ArrayList<>(set.getStoredData());
         if (number == 1) {
             int index = ThreadLocalRandom.current().nextInt(list.size());
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/BZMPop.java b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/BZMPop.java
index b224847..1335e61 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/BZMPop.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/BZMPop.java
@@ -1,7 +1,6 @@
 package com.github.fppt.jedismock.operations.sortedsets;
 
 import com.github.fppt.jedismock.datastructures.Slice;
-import com.github.fppt.jedismock.exception.ArgumentException;
 import com.github.fppt.jedismock.operations.RedisCommand;
 import com.github.fppt.jedismock.storage.OperationExecutorState;
 
@@ -16,14 +15,15 @@ public class BZMPop extends BZPop {
 
     BZMPop(OperationExecutorState state, List<Slice> params) {
         super(state, params);
+    }
 
+    @Override
+    protected int minArgs() {
+        return 4;
     }
 
     @Override
     protected void doOptionalWork(){
-        if (params().size() < 4) {
-            throw new ArgumentException("ERR wrong number of arguments for 'bzmpop' command");
-        }
         timeoutNanos = toNanoTimeout(params().get(0).toString());
         params().remove(0);
         ZMPop zmPop = new ZMPop(base(), new ArrayList<>(params()));
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZIncrBy.java b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZIncrBy.java
index b4f05be..e0e6e87 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZIncrBy.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZIncrBy.java
@@ -2,7 +2,6 @@ package com.github.fppt.jedismock.operations.sortedsets;
 
 import com.github.fppt.jedismock.datastructures.RMZSet;
 import com.github.fppt.jedismock.datastructures.Slice;
-import com.github.fppt.jedismock.exception.ArgumentException;
 import com.github.fppt.jedismock.operations.RedisCommand;
 import com.github.fppt.jedismock.server.Response;
 import com.github.fppt.jedismock.storage.RedisBase;
@@ -16,11 +15,17 @@ public class ZIncrBy extends AbstractByScoreOperation {
     }
 
     @Override
-    protected Slice response() {
-        if (params().size() != 3) {
-            throw new ArgumentException("ERR wrong number of arguments for 'zincrby' command");
-        }
+    protected int minArgs() {
+        return 3;
+    }
 
+    @Override
+    protected int maxArgs() {
+        return 3;
+    }
+
+    @Override
+    protected Slice response() {
         return Response.bulkString(Slice.create(String.valueOf(getNewScore())));
     }
 
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZMPop.java b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZMPop.java
index c8416ca..2c42d56 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZMPop.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZMPop.java
@@ -26,11 +26,13 @@ public class ZMPop extends ZPop {
         super(base, params, false);
     }
 
+    @Override
+    protected int minArgs() {
+        return 3;
+    }
+
     @Override
     protected Slice response() {
-        if (params().size() < 3) {
-            throw new ArgumentException("ERR wrong number of arguments for 'zmpop' command");
-        }
         parseArgs();
         return getResult();
     }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZMScore.java b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZMScore.java
index 906ca36..0ab6f9c 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZMScore.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZMScore.java
@@ -2,7 +2,6 @@ package com.github.fppt.jedismock.operations.sortedsets;
 
 import com.github.fppt.jedismock.datastructures.RMZSet;
 import com.github.fppt.jedismock.datastructures.Slice;
-import com.github.fppt.jedismock.exception.ArgumentException;
 import com.github.fppt.jedismock.operations.AbstractRedisOperation;
 import com.github.fppt.jedismock.operations.RedisCommand;
 import com.github.fppt.jedismock.server.Response;
@@ -18,11 +17,13 @@ class ZMScore extends AbstractRedisOperation {
         super(base, params);
     }
 
+    @Override
+    protected int minArgs() {
+        return 2;
+    }
+
     @Override
     protected Slice response() {
-        if (params().size() < 2) {
-            throw new ArgumentException("*ERR*wrong*number*arg*");
-        }
         Slice key = params().get(0);
 
         final RMZSet mapDBObj = getZSetFromBaseOrCreateEmpty(key);
diff --git a/src/main/java/com/github/fppt/jedismock/operations/streams/Ranges.java b/src/main/java/com/github/fppt/jedismock/operations/streams/Ranges.java
new file mode 100644
index 0000000..1dadada
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/streams/Ranges.java
@@ -0,0 +1,150 @@
+package com.github.fppt.jedismock.operations.streams;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.datastructures.streams.RMStream;
+import com.github.fppt.jedismock.datastructures.streams.SequencedMap;
+import com.github.fppt.jedismock.datastructures.streams.SequencedMapIterator;
+import com.github.fppt.jedismock.datastructures.streams.StreamId;
+import com.github.fppt.jedismock.exception.WrongStreamKeyException;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.NOT_AN_INTEGER_ERROR;
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.SYNTAX_ERROR;
+
+public class Ranges extends AbstractRedisOperation {
+    /**
+     * Multiplier for comparison:<br>
+     * 1 - 'xrange'<br>
+     * -1 - 'xrevrange'
+     */
+    protected int multiplier = 1;
+
+    Ranges(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    protected StreamId preprocessExclusiveBorder(StreamId key, boolean isStart) throws WrongStreamKeyException {
+        if (isStart) {
+            return key.increment();
+        }
+
+        return key.decrement();
+    }
+
+    protected StreamId preprocessKey(Slice key, RMStream stream, boolean isStart) throws WrongStreamKeyException {
+        String rawKey = key.toString();
+
+        if (rawKey.equals("-")) {
+            return stream.getStoredData().getHead();
+        } else if (rawKey.equals("+")) {
+            return stream.getStoredData().getTail();
+        }
+
+        boolean exclusive = false;
+        StreamId id;
+
+        if (rawKey.charAt(0) == '(') {
+            exclusive = true;
+            id = new StreamId(rawKey.substring(1));
+        } else {
+            id = new StreamId(key);
+        }
+
+        if (exclusive) {
+            id = preprocessExclusiveBorder(id, isStart);
+        }
+
+        return id;
+    }
+
+    protected Slice range() {
+        RMStream stream = getStreamFromBaseOrCreateEmpty(params().get(0));
+        SequencedMap<StreamId, SequencedMap<Slice, Slice>> map = stream.getStoredData();
+
+        /* Begin parsing arguments */
+        StreamId start;
+        StreamId end;
+        int count = map.size();
+
+        try {
+            start = preprocessKey(params().get(1), stream, true);
+            end = preprocessKey(params().get(2), stream, false);
+        } catch (WrongStreamKeyException e) {
+            return Response.error(e.getMessage());
+        }
+
+        if (params().size() > 3) {
+            if (params().size() != 5) {
+                return Response.error(SYNTAX_ERROR);
+            }
+
+            if (!"count".equalsIgnoreCase(params().get(3).toString())) {
+                return Response.error(SYNTAX_ERROR);
+            }
+
+            try {
+                count = Integer.parseInt(params().get(4).toString());
+            } catch (NumberFormatException e) {
+                return Response.error(NOT_AN_INTEGER_ERROR);
+            }
+        }
+        /* End parsing arguments */
+
+        if (map.size() == 0) { // empty map case
+            return Response.array(Collections.emptyList());
+        }
+
+        /* Compare with the last item in map */
+        if (multiplier == 1) {
+            if (start.compareTo(map.getTail()) > 0) {
+                return Response.array(Collections.emptyList());
+            }
+        } else {
+            if (start.compareTo(map.getHead()) < 0) {
+                return Response.array(Collections.emptyList());
+            }
+        }
+
+        SequencedMapIterator<StreamId, SequencedMap<Slice, Slice>> it = multiplier == 1
+                ? map.iterator(start)
+                : map.reverseIterator(start);
+
+        List<Slice> output = new ArrayList<>();
+
+        int entriesAdded = 1;
+
+        while (it.hasNext() && entriesAdded++ <= count) {
+            List<Slice> entrySlice = new ArrayList<>();
+            Map.Entry<StreamId, SequencedMap<Slice, Slice>> entry = it.next();
+
+            if (multiplier * entry.getKey().compareTo(end) > 0) {
+                break;
+            }
+
+            entry.getValue().forEach((key, value) -> {
+                entrySlice.add(Response.bulkString(key));
+                entrySlice.add(Response.bulkString(value));
+            });
+
+            output.add(Response.array(Arrays.asList(
+                    Response.bulkString(entry.getKey().toSlice()),
+                    Response.array(entrySlice)
+            )));
+        }
+
+        return Response.array(output);
+    }
+
+    @Override
+    protected Slice response() {
+        return null;
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/streams/XAdd.java b/src/main/java/com/github/fppt/jedismock/operations/streams/XAdd.java
new file mode 100644
index 0000000..f9698f0
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/streams/XAdd.java
@@ -0,0 +1,147 @@
+package com.github.fppt.jedismock.operations.streams;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.datastructures.streams.SequencedMap;
+import com.github.fppt.jedismock.datastructures.streams.RMStream;
+import com.github.fppt.jedismock.datastructures.streams.StreamId;
+import com.github.fppt.jedismock.exception.WrongStreamKeyException;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.List;
+
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.LIMIT_OPTION_ERROR;
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.TOP_ERROR;
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.ZERO_ERROR;
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.SYNTAX_ERROR;
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.NOT_AN_INTEGER_ERROR;
+import static com.github.fppt.jedismock.operations.streams.XTrim.trimID;
+import static com.github.fppt.jedismock.operations.streams.XTrim.trimLen;
+
+/**
+ * XADD key [NOMKSTREAM] [(MAXLEN | MINID) [= | ~] threshold
+ *   [LIMIT count]] (* | id) field value [field value ...]<br>
+ * Supported options: all, except '~'<br>
+ * About trim options see {@link XTrim}
+ */
+@RedisCommand("xadd")
+public class XAdd extends AbstractRedisOperation {
+    public XAdd(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    void validate(StreamId key) throws WrongStreamKeyException {
+        if (key.isZero()) {
+            throw new WrongStreamKeyException(ZERO_ERROR);
+        }
+
+        StreamId lastId = getStreamFromBaseOrCreateEmpty(params().get(0)).getLastId();
+        if (key.compareTo(lastId) <= 0) {
+            throw new WrongStreamKeyException(TOP_ERROR);
+        }
+    }
+
+    @Override
+    protected int minArgs() {
+        return 4;
+    }
+
+    @Override
+    protected Slice response() {
+        Slice key = params().get(0);
+        RMStream stream = getStreamFromBaseOrCreateEmpty(key);
+        SequencedMap<StreamId, SequencedMap<Slice, Slice>> map = stream.getStoredData();
+
+        int idInd = 1; // 'id' index
+
+        /* Parsing NOMSTREAM option */
+        if ("nomkstream".equalsIgnoreCase(params().get(1).toString())) {
+            if (!base().exists(key)) {
+               return Response.NULL;
+            }
+
+            idInd++; // incrementing position
+        }
+
+        /*  Begin trim options parsing */
+        String criterion = ""; // (MAXLEN|MINID) option
+        int thresholdPosition = idInd + 1;
+        int limit = map.size() + 1;
+
+        String param = params().get(idInd).toString();
+        if ("maxlen".equalsIgnoreCase(param) || "minid".equalsIgnoreCase(param)) {
+            criterion = params().get(idInd++).toString();
+
+            param = params().get(idInd++).toString();
+
+            boolean approxTrim = "~".equals(param);
+
+            if ("~".equals(param) || "=".equals(param)) {
+                ++thresholdPosition;
+                ++idInd;
+            }
+
+            if ("limit".equalsIgnoreCase(params().get(idInd).toString())) {
+                try {
+                    limit = Integer.parseInt(params().get(++idInd).toString());
+                } catch (NumberFormatException e) {
+                    return Response.error(NOT_AN_INTEGER_ERROR);
+                }
+
+                if (!approxTrim) {
+                    return Response.error(LIMIT_OPTION_ERROR);
+                }
+
+                ++idInd;
+            }
+        }
+        /*  End trim options parsing */
+
+        Slice id = params().get(idInd++);
+
+
+        StreamId nodeId;
+        try {
+           nodeId = new StreamId(stream.replaceAsterisk(id));
+           validate(nodeId);
+        } catch (WrongStreamKeyException e) {
+            return Response.error(e.getMessage());
+        }
+
+        SequencedMap<Slice, Slice> entryValues = new SequencedMap<>();
+        for (int i = idInd; i < params().size(); i += 2) {
+            entryValues.append(params().get(i), params().get(i + 1));
+        }
+
+        map.append(nodeId, entryValues);
+        stream.updateLastId(nodeId);
+
+        base().putValue(key, stream);
+
+        switch (criterion.toUpperCase()) {
+            case "MAXLEN":
+                try {
+                    int threshold = Integer.parseInt(params().get(thresholdPosition).toString());
+                    trimLen(map, threshold, limit);
+                } catch (NumberFormatException e) {
+                    return Response.error(SYNTAX_ERROR);
+                }
+                break;
+
+            case "MINID":
+                try {
+                    StreamId threshold = new StreamId(params().get(thresholdPosition));
+                    trimID(map, threshold, limit);
+                } catch (WrongStreamKeyException e) {
+                    return Response.error(e.getMessage());
+                }
+                break;
+            default:
+                // ignored
+        }
+
+        return Response.bulkString(nodeId.toSlice());
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/streams/XDel.java b/src/main/java/com/github/fppt/jedismock/operations/streams/XDel.java
new file mode 100644
index 0000000..837e00b
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/streams/XDel.java
@@ -0,0 +1,51 @@
+package com.github.fppt.jedismock.operations.streams;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.datastructures.streams.SequencedMap;
+import com.github.fppt.jedismock.datastructures.streams.StreamId;
+import com.github.fppt.jedismock.exception.WrongStreamKeyException;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * XDEL key id [id ...]<br>
+ */
+@RedisCommand("xdel")
+public class XDel extends AbstractRedisOperation {
+    XDel(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    @Override
+    protected int minArgs() {
+        return 2;
+    }
+
+    @Override
+    protected Slice response() {
+        Slice key = params().get(0);
+        SequencedMap<StreamId, SequencedMap<Slice, Slice>> map = getStreamFromBaseOrCreateEmpty(key).getStoredData();
+
+        List<StreamId> idsToBeDeleted = new ArrayList<>();
+        try {
+            for (int i = 1; i < params().size(); i++) {
+                idsToBeDeleted.add(new StreamId(params().get(i)));
+            }
+        } catch (WrongStreamKeyException e) {
+            return Response.error(e.getMessage());
+        }
+
+        int removedCount = 0;
+        for (StreamId id : idsToBeDeleted) {
+            if (map.remove(id) != null) {
+                removedCount++;
+            }
+        }
+        return Response.integer(removedCount);
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/streams/XLen.java b/src/main/java/com/github/fppt/jedismock/operations/streams/XLen.java
new file mode 100644
index 0000000..7966cac
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/streams/XLen.java
@@ -0,0 +1,34 @@
+package com.github.fppt.jedismock.operations.streams;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.List;
+
+/**
+ * XLEN key
+ */
+@RedisCommand("xlen")
+public class XLen extends AbstractRedisOperation {
+    XLen(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    @Override
+    protected int minArgs() {
+        return 1;
+    }
+
+    @Override
+    protected int maxArgs() {
+        return 1;
+    }
+
+    @Override
+    protected Slice response() {
+        return Response.integer(getStreamFromBaseOrCreateEmpty(params().get(0)).getStoredData().size());
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/streams/XRange.java b/src/main/java/com/github/fppt/jedismock/operations/streams/XRange.java
new file mode 100644
index 0000000..4bcf6ad
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/streams/XRange.java
@@ -0,0 +1,29 @@
+package com.github.fppt.jedismock.operations.streams;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.List;
+
+/**
+ * XRANGE key start end [COUNT count]<br>
+ * Supported options: COUNT
+ */
+@RedisCommand("xrange")
+public class XRange extends Ranges {
+    XRange(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    @Override
+    protected int minArgs() {
+        return 3;
+    }
+
+    @Override
+    protected Slice response() {
+        multiplier = 1;
+        return range();
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/streams/XRead.java b/src/main/java/com/github/fppt/jedismock/operations/streams/XRead.java
new file mode 100644
index 0000000..ad23fb9
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/streams/XRead.java
@@ -0,0 +1,197 @@
+package com.github.fppt.jedismock.operations.streams;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.datastructures.streams.SequencedMap;
+import com.github.fppt.jedismock.datastructures.streams.SequencedMapIterator;
+import com.github.fppt.jedismock.datastructures.streams.StreamId;
+import com.github.fppt.jedismock.exception.WrongStreamKeyException;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.OperationExecutorState;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.NEGATIVE_TIMEOUT_ERROR;
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.SYNTAX_ERROR;
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.XREAD_ARGS_ERROR;
+
+/**
+ * XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id
+ *   [id ...]<br>
+ * All options are supported
+ */
+@RedisCommand("xread")
+public class XRead extends AbstractRedisOperation {
+    private final Object lock;
+    private final boolean isInTransaction;
+
+    public XRead(OperationExecutorState state, List<Slice> params) {
+        super(state.base(), params);
+        lock = state.lock();
+        isInTransaction = state.isTransactionModeOn();
+    }
+
+    @Override
+    protected int minArgs() {
+        return 3;
+    }
+
+    @Override
+    protected Slice response() {
+        int streamInd = 0;
+        int count;
+        long blockTimeNanosec = 0;
+        boolean isBlocking = false;
+
+        if ("count".equalsIgnoreCase(params().get(streamInd).toString())) {
+            count = Integer.parseInt(params().get(++streamInd).toString());
+            ++streamInd;
+        } else {
+            count = Integer.MAX_VALUE;
+        }
+
+        if ("block".equalsIgnoreCase(params().get(streamInd).toString())) {
+            blockTimeNanosec = Long.parseLong(params().get(++streamInd).toString()) * 1_000_000;
+            isBlocking = true;
+
+            if (blockTimeNanosec < 0) {
+                return Response.error(NEGATIVE_TIMEOUT_ERROR);
+            }
+
+            ++streamInd;
+        }
+
+        if (!"streams".equalsIgnoreCase(params().get(streamInd++).toString())) {
+            return Response.error(SYNTAX_ERROR);
+        }
+
+        /* After STREAMS args go in pairs */
+        if ((params().size() - streamInd) % 2 != 0) {
+            return Response.error(XREAD_ARGS_ERROR);
+        }
+
+        int streamsCount = (params().size() - streamInd) / 2;
+
+        SequencedMap<Slice, StreamId> mapKeyToBeginEntryId = new SequencedMap<>();
+
+        /* Mapping all stream ids */
+        for (int i = 0; i < streamsCount; ++i) {
+            Slice key = params().get(streamInd + i);
+            Slice id = params().get(streamInd + streamsCount + i);
+
+            try {
+                if (!base().exists(key)) {
+                    mapKeyToBeginEntryId.append(
+                            key,
+                            "$".equalsIgnoreCase(id.toString())
+                                    ? new StreamId(0, 1) // lowest possible id
+                                    : new StreamId(id)
+                    );
+                } else {
+                    mapKeyToBeginEntryId.append(
+                            key,
+                            "$".equalsIgnoreCase(id.toString())
+                                    /* last id added to stream */
+                                    ? getStreamFromBaseOrCreateEmpty(key).getStoredData().getTail()
+                                    : new StreamId(id)
+                    );
+                }
+            } catch (WrongStreamKeyException e) {
+                return Response.error(e.getMessage());
+            }
+        }
+
+        List<Slice> output = new ArrayList<>();
+
+        /* Blocking */
+        long waitEnd = System.nanoTime() + blockTimeNanosec;
+        long waitTimeNanos;
+
+        if (isBlocking) {
+            if (blockTimeNanosec > 0) {
+                try {
+                    while (!isInTransaction && (waitTimeNanos = waitEnd - System.nanoTime()) >= 0) {
+                        lock.wait(waitTimeNanos / 1_000_000, (int) (waitTimeNanos % 1_000_000));
+                    }
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    return Response.NULL;
+                }
+            } else { // should be blocked until XADD was invoked
+                boolean updated = false;
+
+                try {
+                    while (!isInTransaction && !updated) {
+                        for (Map.Entry<Slice, StreamId> entry : mapKeyToBeginEntryId) {
+                            if (base().exists(entry.getKey())
+                                    && getStreamFromBaseOrCreateEmpty(entry.getKey())
+                                        .getStoredData()
+                                        .getTail()
+                                        .compareTo(entry.getValue()) > 0) {
+                                updated = true;
+                                break;
+                            }
+                        }
+                        lock.wait(500, 0);
+                    }
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    return Response.NULL;
+                }
+            }
+        }
+
+        /* Response */
+        mapKeyToBeginEntryId.forEach((key, id) -> {
+            SequencedMap<StreamId, SequencedMap<Slice, Slice>> map = getStreamFromBaseOrCreateEmpty(key).getStoredData();
+            SequencedMapIterator<StreamId, SequencedMap<Slice, Slice>> it;
+
+            if (!base().exists(key)) {
+                return; // skip
+            }
+
+
+            if (map.getTail() == null || id.compareTo(map.getTail()) >= 0) {
+                return; // skip
+            }
+
+            try {
+                id = id.increment();
+            } catch (WrongStreamKeyException e) {
+                return; // impossible as 0xFFFFFFFFFFFFFFFF is greater or equal to all keys
+            }
+
+            it = map.iterator(id);
+
+            List<Slice> data = new ArrayList<>();
+            int addedEntries = 1;
+
+            while (it.hasNext() && addedEntries++ <= count) {
+                Map.Entry<StreamId, SequencedMap<Slice, Slice>> entry = it.next();
+
+                List<Slice> values = new ArrayList<>();
+
+                entry.getValue().forEach((k, v) -> {
+                    values.add(Response.bulkString(k));
+                    values.add(Response.bulkString(v));
+                });
+
+                data.add(Response.array(Arrays.asList(
+                        Response.bulkString(entry.getKey().toSlice()),
+                        Response.array(values)
+                )));
+            }
+
+            output.add(Response.array(Arrays.asList(
+                    Response.bulkString(key),
+                    Response.array(data)
+            )));
+        });
+
+        return Response.array(output);
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/streams/XRevRange.java b/src/main/java/com/github/fppt/jedismock/operations/streams/XRevRange.java
new file mode 100644
index 0000000..2e0cf14
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/streams/XRevRange.java
@@ -0,0 +1,25 @@
+package com.github.fppt.jedismock.operations.streams;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.List;
+
+@RedisCommand("xrevrange")
+public class XRevRange extends Ranges {
+    public XRevRange(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    @Override
+    protected int minArgs() {
+        return 3;
+    }
+
+    @Override
+    protected Slice response() {
+        multiplier = -1;
+        return range();
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/streams/XTrim.java b/src/main/java/com/github/fppt/jedismock/operations/streams/XTrim.java
new file mode 100644
index 0000000..6ef0323
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/streams/XTrim.java
@@ -0,0 +1,121 @@
+package com.github.fppt.jedismock.operations.streams;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.datastructures.streams.SequencedMap;
+import com.github.fppt.jedismock.datastructures.streams.SequencedMapIterator;
+import com.github.fppt.jedismock.datastructures.streams.StreamId;
+import com.github.fppt.jedismock.exception.WrongStreamKeyException;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.List;
+
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.LIMIT_OPTION_ERROR;
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.NOT_AN_INTEGER_ERROR;
+import static com.github.fppt.jedismock.datastructures.streams.StreamErrors.SYNTAX_ERROR;
+
+/**
+ * XTRIM key (MAXLEN | MINID) [= | ~] threshold [LIMIT count]<br>
+ * Supported options: MINID, MAXLEN, LIMIT, =<br>
+ * Unsupported options: "~" - due to the fact that our implementation works as = option
+ */
+@RedisCommand("xtrim")
+public class XTrim extends AbstractRedisOperation {
+    public XTrim(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    static int trimLen(SequencedMap<?, ?> map, int threshold, int limit) {
+        int numberOfEvictedNodes = 0;
+
+        while (map.size() > threshold && numberOfEvictedNodes < limit) {
+            ++numberOfEvictedNodes;
+            map.removeHead();
+        }
+
+        return numberOfEvictedNodes;
+    }
+
+    static int trimID(SequencedMap<StreamId, ?> map, StreamId threshold, int limit) {
+        int numberOfEvictedNodes = 0;
+        SequencedMapIterator<StreamId, ?> it = map.iterator();
+
+        while (it.hasNext() && numberOfEvictedNodes < limit) {
+            if (it.next().getKey().compareTo(threshold) >= 0) {
+                break;
+            }
+
+            ++numberOfEvictedNodes;
+            it.remove();
+        }
+
+        return numberOfEvictedNodes;
+    }
+
+    @Override
+    protected int minArgs() {
+        return 3;
+    }
+
+    @Override
+    protected Slice response() {
+        /* Begin parsing arguments */
+        Slice key = params().get(0);
+        SequencedMap<StreamId, SequencedMap<Slice, Slice>> map = getStreamFromBaseOrCreateEmpty(key).getStoredData();
+
+        String criterion = params().get(1).toString(); // (MAXLEN|MINID) option
+        int thresholdPosition = 2;
+
+        String param = params().get(2).toString();
+
+        /* Checking for "=", "~" options */
+        if ("~".equals(param) || "=".equals(param)) {
+            ++thresholdPosition;
+        }
+
+        boolean aproxTrim = "~".equals(param);
+
+        int limit = map.size() + 1;
+
+        if (params().size() > thresholdPosition + 3) {
+            return Response.error(SYNTAX_ERROR);
+        }
+
+        if (params().size() > thresholdPosition + 1) {
+            if ("limit".equalsIgnoreCase(params().get(thresholdPosition + 1).toString())) {
+                try {
+                    limit = Integer.parseInt(params().get(thresholdPosition + 2).toString());
+                } catch (NumberFormatException e) {
+                    return Response.error(NOT_AN_INTEGER_ERROR);
+                }
+
+                if (!aproxTrim) {
+                    return Response.error(LIMIT_OPTION_ERROR);
+                }
+            } else {
+                return Response.error(SYNTAX_ERROR);
+            }
+        }
+
+        switch (criterion.toUpperCase()) {
+            case "MAXLEN":
+                try {
+                    int threshold = Integer.parseInt(params().get(thresholdPosition).toString());
+                    return Response.integer(trimLen(map, threshold, limit));
+                } catch (NumberFormatException e) {
+                    return Response.error(SYNTAX_ERROR);
+                }
+            case "MINID":
+                try {
+                    StreamId threshold = new StreamId(params().get(thresholdPosition));
+                    return Response.integer(trimID(map, threshold, limit));
+                } catch (WrongStreamKeyException e) {
+                    return Response.error(e.getMessage());
+                }
+            default:
+                return Response.error(SYNTAX_ERROR);
+        }
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/strings/Set.java b/src/main/java/com/github/fppt/jedismock/operations/strings/Set.java
index 1ee8168..db7b728 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/strings/Set.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/strings/Set.java
@@ -23,11 +23,14 @@ class Set extends AbstractRedisOperation {
     protected Slice response() {
         Slice key = params().get(0);
         Slice value = params().get(1);
-
+        Long ttl = ttl();
+        if (ttl != null && ttl < 0){
+            return Response.error("ERR invalid expire time in 'set' command");
+        }
         if (nx()) {
             Slice old = base().getSlice(key);
             if (old == null) {
-                base().putValue(key, value.extract(), ttl());
+                base().putValue(key, value.extract(), ttl);
                 return Response.OK;
             } else {
                 return Response.NULL;
@@ -37,11 +40,11 @@ class Set extends AbstractRedisOperation {
             if (old == null) {
                 return Response.NULL;
             } else {
-                base().putValue(key, value.extract(), ttl());
+                base().putValue(key, value.extract(), ttl);
                 return Response.OK;
             }
         } else {
-            base().putValue(key, value.extract(), ttl());
+            base().putValue(key, value.extract(), ttl);
             return Response.OK;
         }
     }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/strings/SetEx.java b/src/main/java/com/github/fppt/jedismock/operations/strings/SetEx.java
index e1b4504..c4fdf39 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/strings/SetEx.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/strings/SetEx.java
@@ -1,6 +1,6 @@
 package com.github.fppt.jedismock.operations.strings;
 
-import com  .github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.operations.RedisCommand;
 import com.github.fppt.jedismock.storage.RedisBase;
 import com.github.fppt.jedismock.server.Response;
 import com.github.fppt.jedismock.datastructures.Slice;
@@ -15,12 +15,16 @@ class SetEx extends Set {
         super(base, params);
     }
 
-    long timeoutToSet(List<Slice> params){
+    long timeoutToSet(List<Slice> params) {
         return convertToLong(new String(params.get(1).data())) * 1000;
     }
 
     protected Slice response() {
-        base().putValue(params().get(0), params().get(2).extract(), timeoutToSet(params()));
+        final long timeout = timeoutToSet(params());
+        if (timeout < 0) {
+            return Response.error("ERR invalid expire time in 'setex' command");
+        }
+        base().putValue(params().get(0), params().get(2).extract(), timeout);
         return Response.OK;
     }
 }
diff --git a/src/main/java/com/github/fppt/jedismock/server/RedisClient.java b/src/main/java/com/github/fppt/jedismock/server/RedisClient.java
index baffe75..194c53b 100644
--- a/src/main/java/com/github/fppt/jedismock/server/RedisClient.java
+++ b/src/main/java/com/github/fppt/jedismock/server/RedisClient.java
@@ -81,7 +81,7 @@ public final class RedisClient implements Runnable {
     /**
      * Send a response due to a specific command.
      *
-     * @param response     The respond to send.
+     * @param response     The response to send.
      * @param respondingTo The reason for sending this response
      */
     public void sendResponse(Slice response, String respondingTo) {
diff --git a/src/main/java/com/github/fppt/jedismock/server/Response.java b/src/main/java/com/github/fppt/jedismock/server/Response.java
index 2b61ec2..0c450b7 100644
--- a/src/main/java/com/github/fppt/jedismock/server/Response.java
+++ b/src/main/java/com/github/fppt/jedismock/server/Response.java
@@ -17,6 +17,7 @@ public class Response {
     private static final String LINE_SEPARATOR = "\r\n";
 
     public static final Slice OK = Slice.create("+OK" + LINE_SEPARATOR);
+    public static final Slice QUEUED = Slice.create("+QUEUED" + LINE_SEPARATOR);
     public static final Slice NULL = Slice.create("$-1" + LINE_SEPARATOR);
     public static final Slice SKIP = Slice.create("Skip this submission");
     public static final Slice EMPTY_ARRAY = Response.array(Collections.emptyList());
diff --git a/src/main/java/com/github/fppt/jedismock/storage/RedisBase.java b/src/main/java/com/github/fppt/jedismock/storage/RedisBase.java
index 0afafc3..6d24c37 100644
--- a/src/main/java/com/github/fppt/jedismock/storage/RedisBase.java
+++ b/src/main/java/com/github/fppt/jedismock/storage/RedisBase.java
@@ -7,6 +7,7 @@ import com.github.fppt.jedismock.datastructures.RMHash;
 import com.github.fppt.jedismock.datastructures.RMHyperLogLog;
 import com.github.fppt.jedismock.datastructures.RMList;
 import com.github.fppt.jedismock.datastructures.RMSet;
+import com.github.fppt.jedismock.datastructures.streams.RMStream;
 import com.github.fppt.jedismock.datastructures.RMString;
 import com.github.fppt.jedismock.datastructures.RMZSet;
 import com.github.fppt.jedismock.datastructures.Slice;
@@ -64,6 +65,10 @@ public class RedisBase {
         return null;
     }
 
+    public RMStream getStream(Slice key) {
+        return getStructure(key, RMStream.class);
+    }
+
     public RMSet getSet(Slice key) {
         return getStructure(key, RMSet.class);
     }
diff --git a/supported_operations.md b/supported_operations.md
index 7d1211c..b097142 100644
--- a/supported_operations.md
+++ b/supported_operations.md
@@ -234,20 +234,20 @@
 ## Streams
 
 :x: xack<br>
-:x: xadd<br>
+:heavy_check_mark: xadd<br>
 :x: xautoclaim<br>
 :x: xclaim<br>
-:x: xdel<br>
+:heavy_check_mark: xdel<br>
 :x: xgroup<br>
 :x: xinfo<br>
-:x: xlen<br>
+:heavy_check_mark: xlen<br>
 :x: xpending<br>
-:x: xrange<br>
-:x: xread<br>
+:heavy_check_mark: xrange<br>
+:heavy_check_mark: xread<br>
 :x: xreadgroup<br>
-:x: xrevrange<br>
+:heavy_check_mark: xrevrange<br>
 :x: xsetid<br>
-:x: xtrim<br>
+:heavy_check_mark: xtrim<br>
 
 ## Strings
 


Test patch:
diff --git a/.github/workflows/native-tests.yml b/.github/workflows/native-tests.yml
index 6a1dff5..cd023e8 100644
--- a/.github/workflows/native-tests.yml
+++ b/.github/workflows/native-tests.yml
@@ -78,3 +78,8 @@ jobs:
         if: always()
         run: ./testnative unit/type/zset
         working-directory: native_tests
+
+      - name: stream
+        if: always()
+        run: ./testnative unit/type/stream
+        working-directory: native_tests
diff --git a/native_tests/jm-test-server/pom.xml b/native_tests/jm-test-server/pom.xml
index e1a24f4..1b0ba3b 100644
--- a/native_tests/jm-test-server/pom.xml
+++ b/native_tests/jm-test-server/pom.xml
@@ -1,97 +1,97 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-
-    <groupId>com.github.fppt</groupId>
-    <artifactId>jm-test-server</artifactId>
-    <version>1.0-SNAPSHOT</version>
-
-    <packaging>jar</packaging>
-
-    <properties>
-        <java.version>11</java.version>
-        <maven.compiler.source>${java.version}</maven.compiler.source>
-        <maven.compiler.target>${java.version}</maven.compiler.target>
-        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-        <junit.version>5.9.0</junit.version>
-        <assertj.core.version>3.23.1</assertj.core.version>
-        <approvaltests.version>18.2.2</approvaltests.version>
-        <maven.surefire.plugin.version>2.22.2</maven.surefire.plugin.version>
-        <jacoco.maven.plugin.version>0.8.8</jacoco.maven.plugin.version>
-        <checkstyle.maven.plugin.version>3.2.0</checkstyle.maven.plugin.version>
-        <checkstyle.version>10.3.3</checkstyle.version>
-    </properties>
-
-    <dependencies>
-        <dependency>
-            <groupId>com.github.fppt</groupId>
-            <artifactId>jedis-mock</artifactId>
-            <version>${jedismock.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>ch.qos.logback</groupId>
-            <artifactId>logback-classic</artifactId>
-            <version>1.4.5</version>
-        </dependency>
-    </dependencies>
-
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-assembly-plugin</artifactId>
-                <version>3.4.2</version>
-
-                <configuration>
-                    <archive>
-                        <manifest>
-                            <mainClass>com.github.fppt.jedismock.InterceptorMockServer</mainClass>
-                        </manifest>
-                    </archive>
-                    <descriptorRefs>
-                        <descriptorRef>jar-with-dependencies</descriptorRef>
-                    </descriptorRefs>
-
-                </configuration>
-
-                <executions>
-                    <execution>
-                        <id>make-assembly</id>
-                        <phase>package</phase>
-                        <goals>
-                            <goal>single</goal>
-                        </goals>
-                    </execution>
-                </executions>
-
-            </plugin>
-
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-checkstyle-plugin</artifactId>
-                <version>${checkstyle.maven.plugin.version}</version>
-                <executions>
-                    <execution>
-                        <id>verify-style</id>
-                        <phase>process-classes</phase>
-                        <goals>
-                            <goal>check</goal>
-                        </goals>
-                    </execution>
-                </executions>
-                <configuration>
-                    <configLocation>checkstyle.xml</configLocation>
-                </configuration>
-                <dependencies>
-                    <dependency>
-                        <groupId>com.puppycrawl.tools</groupId>
-                        <artifactId>checkstyle</artifactId>
-                        <version>${checkstyle.version}</version>
-                    </dependency>
-                </dependencies>
-            </plugin>
-        </plugins>
-    </build>
-</project>
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.github.fppt</groupId>
+    <artifactId>jm-test-server</artifactId>
+    <version>1.0-SNAPSHOT</version>
+
+    <packaging>jar</packaging>
+
+    <properties>
+        <java.version>11</java.version>
+        <maven.compiler.source>${java.version}</maven.compiler.source>
+        <maven.compiler.target>${java.version}</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <junit.version>5.9.0</junit.version>
+        <assertj.core.version>3.23.1</assertj.core.version>
+        <approvaltests.version>18.2.2</approvaltests.version>
+        <maven.surefire.plugin.version>2.22.2</maven.surefire.plugin.version>
+        <jacoco.maven.plugin.version>0.8.8</jacoco.maven.plugin.version>
+        <checkstyle.maven.plugin.version>3.2.0</checkstyle.maven.plugin.version>
+        <checkstyle.version>10.3.3</checkstyle.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.github.fppt</groupId>
+            <artifactId>jedis-mock</artifactId>
+            <version>${jedismock.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>1.4.5</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <version>3.4.2</version>
+
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>com.github.fppt.jedismock.InterceptorMockServer</mainClass>
+                        </manifest>
+                    </archive>
+                    <descriptorRefs>
+                        <descriptorRef>jar-with-dependencies</descriptorRef>
+                    </descriptorRefs>
+
+                </configuration>
+
+                <executions>
+                    <execution>
+                        <id>make-assembly</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
+
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-checkstyle-plugin</artifactId>
+                <version>${checkstyle.maven.plugin.version}</version>
+                <executions>
+                    <execution>
+                        <id>verify-style</id>
+                        <phase>process-classes</phase>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <configLocation>checkstyle.xml</configLocation>
+                </configuration>
+                <dependencies>
+                    <dependency>
+                        <groupId>com.puppycrawl.tools</groupId>
+                        <artifactId>checkstyle</artifactId>
+                        <version>${checkstyle.version}</version>
+                    </dependency>
+                </dependencies>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/native_tests/linux/tests/unit/type/stream.tcl b/native_tests/linux/tests/unit/type/stream.tcl
index 0ff570c..c193746 100644
--- a/native_tests/linux/tests/unit/type/stream.tcl
+++ b/native_tests/linux/tests/unit/type/stream.tcl
@@ -52,6 +52,12 @@ set content {} ;# Will be populated with Tcl side copy of the stream content.
 start_server {
     tags {"stream"}
 } {
+    test "XADD wrong number of args" {
+        assert_error {*wrong number of arguments for 'xadd' command} {r XADD mystream}
+        assert_error {*wrong number of arguments for 'xadd' command} {r XADD mystream *}
+        assert_error {*wrong number of arguments for 'xadd' command} {r XADD mystream * field}
+    }
+
     test {XADD can add entries into a stream that XRANGE can fetch} {
         r XADD mystream * item 1 value a
         r XADD mystream * item 2 value b
@@ -85,6 +91,43 @@ start_server {
         assert_error ERR* {r xadd mystream * c d}
     }
 
+    test {XADD auto-generated sequence is incremented for last ID} {
+        r DEL mystream
+        set id1 [r XADD mystream 123-456 item 1 value a]
+        set id2 [r XADD mystream 123-* item 2 value b]
+        lassign [split $id2 -] _ seq
+        assert {$seq == 457}
+        assert {[streamCompareID $id1 $id2] == -1}
+    }
+
+    test {XADD auto-generated sequence is zero for future timestamp ID} {
+        r DEL mystream
+        set id1 [r XADD mystream 123-456 item 1 value a]
+        set id2 [r XADD mystream 789-* item 2 value b]
+        lassign [split $id2 -] _ seq
+        assert {$seq == 0}
+        assert {[streamCompareID $id1 $id2] == -1}
+    }
+
+    test {XADD auto-generated sequence can't be smaller than last ID} {
+        r DEL mystream
+        r XADD mystream 123-456 item 1 value a
+        assert_error ERR* {r XADD mystream 42-* item 2 value b}
+    }
+
+    test {XADD auto-generated sequence can't overflow} {
+        r DEL mystream
+        r xadd mystream 1-18446744073709551615 a b
+        assert_error ERR* {r xadd mystream 1-* c d}
+    }
+
+    test {XADD 0-* should succeed} {
+        r DEL mystream
+        set id [r xadd mystream 0-* a b]
+        lassign [split $id -] _ seq
+        assert {$seq == 1}
+    }
+
     test {XADD with MAXLEN option} {
         r DEL mystream
         for {set j 0} {$j < 1000} {incr j} {
@@ -94,6 +137,7 @@ start_server {
                 r XADD mystream MAXLEN 5 * yitem $j
             }
         }
+        assert {[r xlen mystream] == 5}
         set res [r xrange mystream - +]
         set expected 995
         foreach r $res {
@@ -102,19 +146,102 @@ start_server {
         }
     }
 
-    test {XADD mass insertion and XLEN} {
+    test {XADD with MAXLEN option and the '=' argument} {
+        r DEL mystream
+        for {set j 0} {$j < 1000} {incr j} {
+            if {rand() < 0.9} {
+                r XADD mystream MAXLEN = 5 * xitem $j
+            } else {
+                r XADD mystream MAXLEN = 5 * yitem $j
+            }
+        }
+        assert {[r XLEN mystream] == 5}
+    }
+
+#    test {XADD with MAXLEN option and the '~' argument} {
+#        r DEL mystream
+#        r config set stream-node-max-entries 100
+#        for {set j 0} {$j < 1000} {incr j} {
+#            if {rand() < 0.9} {
+#                r XADD mystream MAXLEN ~ 555 * xitem $j
+#            } else {
+#                r XADD mystream MAXLEN ~ 555 * yitem $j
+#            }
+#        }
+#        assert {[r XLEN mystream] == 600}
+#    }
+
+    test {XADD with NOMKSTREAM option} {
+        r DEL mystream
+        assert_equal "" [r XADD mystream NOMKSTREAM * item 1 value a]
+        assert_equal 0 [r EXISTS mystream]
+        r XADD mystream * item 1 value a
+        r XADD mystream NOMKSTREAM * item 2 value b
+        assert_equal 2 [r XLEN mystream]
+        set items [r XRANGE mystream - +]
+        assert_equal [lindex $items 0 1] {item 1 value a}
+        assert_equal [lindex $items 1 1] {item 2 value b}
+    }
+
+    test {XADD with MINID option} {
         r DEL mystream
+        for {set j 1} {$j < 1001} {incr j} {
+            set minid 1000
+            if {$j >= 5} {
+                set minid [expr {$j-5}]
+            }
+            if {rand() < 0.9} {
+                r XADD mystream MINID $minid $j xitem $j
+            } else {
+                r XADD mystream MINID $minid $j yitem $j
+            }
+        }
+        assert {[r xlen mystream] == 6}
+        set res [r xrange mystream - +]
+        set expected 995
+        foreach r $res {
+            assert {[lindex $r 1 1] == $expected}
+            incr expected
+        }
+    }
+
+    test {XTRIM with MINID option} {
+        r DEL mystream
+        r XADD mystream 1-0 f v
+        r XADD mystream 2-0 f v
+        r XADD mystream 3-0 f v
+        r XADD mystream 4-0 f v
+        r XADD mystream 5-0 f v
+        r XTRIM mystream MINID = 3-0
+        assert_equal [r XRANGE mystream - +] {{3-0 {f v}} {4-0 {f v}} {5-0 {f v}}}
+    }
+
+    test {XTRIM with MINID option, big delta from master record} {
+        r DEL mystream
+        r XADD mystream 1-0 f v
+        r XADD mystream 1641544570597-0 f v
+        r XADD mystream 1641544570597-1 f v
+        r XTRIM mystream MINID 1641544570597-0
+        assert_equal [r XRANGE mystream - +] {{1641544570597-0 {f v}} {1641544570597-1 {f v}}}
+    }
+
+    proc insert_into_stream_key {key {count 10000}} {
         r multi
-        for {set j 0} {$j < 10000} {incr j} {
+        for {set j 0} {$j < $count} {incr j} {
             # From time to time insert a field with a different set
             # of fields in order to stress the stream compression code.
             if {rand() < 0.9} {
-                r XADD mystream * item $j
+                r XADD $key * item $j
             } else {
-                r XADD mystream * item $j otherfield foo
+                r XADD $key * item $j otherfield foo
             }
         }
         r exec
+    }
+
+    test {XADD mass insertion and XLEN} {
+        r DEL mystream
+        insert_into_stream_key mystream
 
         set items [r XRANGE mystream - +]
         for {set j 0} {$j < 10000} {incr j} {
@@ -129,6 +256,18 @@ start_server {
         assert {[r EXISTS otherstream] == 0}
     }
 
+# WARNING! Our implementation removes exactly limit
+#    test {XADD with LIMIT delete entries no more than limit} {
+#        r del yourstream
+#        for {set j 0} {$j < 300} {incr j} {
+#            r XADD yourstream * xitem v
+#        }
+#        puts [r xrange yourstream - +]
+#        r XADD yourstream MAXLEN ~ 0 limit 1 * xitem v
+#        puts [r xrange yourstream - +]
+#        assert {[r XLEN yourstream] == 4}
+#    }
+
     test {XRANGE COUNT works as expected} {
         assert {[llength [r xrange mystream - + COUNT 10]] == 10}
     }
@@ -156,39 +295,69 @@ start_server {
         assert {[r xrange mystream - +] == [lreverse [r xrevrange mystream + -]]}
     }
 
+    test {XRANGE exclusive ranges} {
+        set ids {0-1 0-18446744073709551615 1-0 42-0 42-42
+                 18446744073709551615-18446744073709551614
+                 18446744073709551615-18446744073709551615}
+        set total [llength $ids]
+        r multi
+        r DEL vipstream
+        foreach id $ids {
+            r XADD vipstream $id foo bar
+        }
+        r exec
+        assert {[llength [r xrange vipstream - +]] == $total}
+        assert {[llength [r xrange vipstream ([lindex $ids 0] +]] == $total-1}
+        assert {[llength [r xrange vipstream - ([lindex $ids $total-1]]] == $total-1}
+        assert {[llength [r xrange vipstream (0-1 (1-0]] == 1}
+        assert {[llength [r xrange vipstream (1-0 (42-42]] == 1}
+        catch {r xrange vipstream (- +} e
+        assert_match {ERR*} $e
+        catch {r xrange vipstream - (+} e
+        assert_match {ERR*} $e
+        catch {r xrange vipstream (18446744073709551615-18446744073709551615 +} e
+        assert_match {ERR*} $e
+        catch {r xrange vipstream - (0-0} e
+        assert_match {ERR*} $e
+    }
+
     test {XREAD with non empty stream} {
         set res [r XREAD COUNT 1 STREAMS mystream 0-0]
         assert {[lrange [lindex $res 0 1 0 1] 0 1] eq {item 0}}
     }
 
     test {Non blocking XREAD with empty streams} {
-        set res [r XREAD STREAMS s1 s2 0-0 0-0]
+        set res [r XREAD STREAMS s1{t} s2{t} 0-0 0-0]
         assert {$res eq {}}
     }
 
     test {XREAD with non empty second stream} {
-        set res [r XREAD COUNT 1 STREAMS nostream mystream 0-0 0-0]
-        assert {[lindex $res 0 0] eq {mystream}}
+        insert_into_stream_key mystream{t}
+        set res [r XREAD COUNT 1 STREAMS nostream{t} mystream{t} 0-0 0-0]
+        assert {[lindex $res 0 0] eq {mystream{t}}}
         assert {[lrange [lindex $res 0 1 0 1] 0 1] eq {item 0}}
     }
 
     test {Blocking XREAD waiting new data} {
-        r XADD s2 * old abcd1234
+        r XADD s2{t} * old abcd1234
         set rd [redis_deferring_client]
-        $rd XREAD BLOCK 20000 STREAMS s1 s2 s3 $ $ $
-        r XADD s2 * new abcd1234
+        $rd XREAD BLOCK 20000 STREAMS s1{t} s2{t} s3{t} $ $ $
+        wait_for_blocked_client
+        r XADD s2{t} * new abcd1234
         set res [$rd read]
-        assert {[lindex $res 0 0] eq {s2}}
+        assert {[lindex $res 0 0] eq {s2{t}}}
         assert {[lindex $res 0 1 0 1] eq {new abcd1234}}
+        $rd close
     }
 
     test {Blocking XREAD waiting old data} {
         set rd [redis_deferring_client]
-        $rd XREAD BLOCK 20000 STREAMS s1 s2 s3 $ 0-0 $
-        r XADD s2 * foo abcd1234
+        $rd XREAD BLOCK 20000 STREAMS s1{t} s2{t} s3{t} $ 0-0 $
+        r XADD s2{t} * foo abcd1234
         set res [$rd read]
-        assert {[lindex $res 0 0] eq {s2}}
+        assert {[lindex $res 0 0] eq {s2{t}}}
         assert {[lindex $res 0 1 0 1] eq {old abcd1234}}
+        $rd close
     }
 
     test {Blocking XREAD will not reply with an empty array} {
@@ -200,52 +369,87 @@ start_server {
         $rd XREAD BLOCK 10 STREAMS s1 666
         after 20
         assert {[$rd read] == {}} ;# before the fix, client didn't even block, but was served synchronously with {s1 {}}
-    }
-
-    test "XREAD: XADD + DEL should not awake client" {
-        set rd [redis_deferring_client]
-        r del s1
-        $rd XREAD BLOCK 20000 STREAMS s1 $
-        r multi
-        r XADD s1 * old abcd1234
-        r DEL s1
-        r exec
-        r XADD s1 * new abcd1234
-        set res [$rd read]
-        assert {[lindex $res 0 0] eq {s1}}
-        assert {[lindex $res 0 1 0 1] eq {new abcd1234}}
-    }
-
-    test "XREAD: XADD + DEL + LPUSH should not awake client" {
-        set rd [redis_deferring_client]
-        r del s1
-        $rd XREAD BLOCK 20000 STREAMS s1 $
-        r multi
-        r XADD s1 * old abcd1234
-        r DEL s1
-        r LPUSH s1 foo bar
-        r exec
-        r DEL s1
-        r XADD s1 * new abcd1234
-        set res [$rd read]
-        assert {[lindex $res 0 0] eq {s1}}
-        assert {[lindex $res 0 1 0 1] eq {new abcd1234}}
-    }
-
+        $rd close
+    }
+
+#    test "Blocking XREAD for stream that ran dry (issue 5299)" {
+#        set rd [redis_deferring_client]
+#
+#        # Add a entry then delete it, now stream's last_id is 666.
+#        r DEL mystream
+#        r XADD mystream 666 key value
+#        r XDEL mystream 666
+#
+#        # Pass a ID smaller than stream's last_id, released on timeout.
+#        $rd XREAD BLOCK 10 STREAMS mystream 665
+#        assert_equal [$rd read] {}
+#
+#        # Throw an error if the ID equal or smaller than the last_id.
+#        assert_error ERR*equal*smaller* {r XADD mystream 665 key value}
+#        assert_error ERR*equal*smaller* {r XADD mystream 666 key value}
+#
+#        # Entered blocking state and then release because of the new entry.
+#        $rd XREAD BLOCK 0 STREAMS mystream 665
+#        wait_for_blocked_clients_count 1
+#        r XADD mystream 667 key value
+#        assert_equal [$rd read] {{mystream {{667-0 {key value}}}}}
+#
+#        $rd close
+#    }
+#
+#    test "XREAD: XADD + DEL should not awake client" {
+#        set rd [redis_deferring_client]
+#        r del s1
+#        $rd XREAD BLOCK 20000 STREAMS s1 $
+#        wait_for_blocked_clients_count 1
+#        r multi
+#        r XADD s1 * old abcd1234
+#        r DEL s1
+#        r exec
+#        r XADD s1 * new abcd1234
+#        set res [$rd read]
+#        assert {[lindex $res 0 0] eq {s1}}
+#        assert {[lindex $res 0 1 0 1] eq {new abcd1234}}
+#        $rd close
+#    }
+#
+#    test "XREAD: XADD + DEL + LPUSH should not awake client" {
+#        set rd [redis_deferring_client]
+#        r del s1
+#        $rd XREAD BLOCK 20000 STREAMS s1 $
+#        wait_for_blocked_clients_count 1
+#        r multi
+#        r XADD s1 * old abcd1234
+#        r DEL s1
+#        r LPUSH s1 foo bar
+#        r exec
+#        r DEL s1
+#        r XADD s1 * new abcd1234
+#        set res [$rd read]
+#        assert {[lindex $res 0 0] eq {s1}}
+#        assert {[lindex $res 0 1 0 1] eq {new abcd1234}}
+#        $rd close
+#    }
+#
     test {XREAD with same stream name multiple times should work} {
         r XADD s2 * old abcd1234
         set rd [redis_deferring_client]
         $rd XREAD BLOCK 20000 STREAMS s2 s2 s2 $ $ $
+        # wait_for_blocked_clients_count 1 -> wait_for_blocked_client
+        wait_for_blocked_client
         r XADD s2 * new abcd1234
         set res [$rd read]
         assert {[lindex $res 0 0] eq {s2}}
         assert {[lindex $res 0 1 0 1] eq {new abcd1234}}
+        $rd close
     }
 
     test {XREAD + multiple XADD inside transaction} {
         r XADD s2 * old abcd1234
         set rd [redis_deferring_client]
         $rd XREAD BLOCK 20000 STREAMS s2 s2 s2 $ $ $
+        # wait_for_blocked_clients_count 1 -> wait_for_blocked_client
+        wait_for_blocked_client
         r MULTI
         r XADD s2 * field one
         r XADD s2 * field two
@@ -255,6 +459,7 @@ start_server {
         assert {[lindex $res 0 0] eq {s2}}
         assert {[lindex $res 0 1 0 1] eq {field one}}
         assert {[lindex $res 0 1 1 1] eq {field two}}
+       $rd close
     }
 
     test {XDEL basic test} {
@@ -269,34 +474,48 @@ start_server {
         assert {[lindex $result 1 1 1] eq {value2}}
     }
 
-    # Here the idea is to check the consistency of the stream data structure
-    # as we remove all the elements down to zero elements.
+    test {XDEL multiply id test} {
+        r del somestream
+        r xadd somestream 1-1 a 1
+        r xadd somestream 1-2 b 2
+        r xadd somestream 1-3 c 3
+        r xadd somestream 1-4 d 4
+        r xadd somestream 1-5 e 5
+        assert {[r xlen somestream] == 5}
+        assert {[r xdel somestream 1-1 1-4 1-5 2-1] == 3}
+        assert {[r xlen somestream] == 2}
+        set result [r xrange somestream - +]
+        assert {[dict get [lindex $result 0 1] b] eq {2}}
+        assert {[dict get [lindex $result 1 1] c] eq {3}}
+    }
+#     Here the idea is to check the consistency of the stream data structure
+#     as we remove all the elements down to zero elements.
     test {XDEL fuzz test} {
         r del somestream
         set ids {}
         set x 0; # Length of the stream
-        while 1 {
+        while {$x < 100} { # WARNING! Modified condition as Jedis Mock does not implement XINFO command
             lappend ids [r xadd somestream * item $x]
             incr x
-            # Add enough elements to have a few radix tree nodes inside the stream.
-            if {[dict get [r xinfo stream somestream] radix-tree-keys] > 20} break
+#             Add enough elements to have a few radix tree nodes inside the stream.
+#            if {[dict get [r xinfo stream somestream] radix-tree-keys] > 20} break
         }
 
-        # Now remove all the elements till we reach an empty stream
-        # and after every deletion, check that the stream is sane enough
-        # to report the right number of elements with XRANGE: this will also
-        # force accessing the whole data structure to check sanity.
+#         Now remove all the elements till we reach an empty stream
+#         and after every deletion, check that the stream is sane enough
+#         to report the right number of elements with XRANGE: this will also
+#         force accessing the whole data structure to check sanity.
         assert {[r xlen somestream] == $x}
 
-        # We want to remove elements in random order to really test the
-        # implementation in a better way.
+#         We want to remove elements in random order to really test the
+#         implementation in a better way.
         set ids [lshuffle $ids]
         foreach id $ids {
             assert {[r xdel somestream $id] == 1}
             incr x -1
             assert {[r xlen somestream] == $x}
-            # The test would be too slow calling XRANGE for every iteration.
-            # Do it every 100 removal.
+#             The test would be too slow calling XRANGE for every iteration.
+#             Do it every 100 removal.
             if {$x % 100 == 0} {
                 set res [r xrange somestream - +]
                 assert {[llength $res] == $x}
@@ -305,12 +524,14 @@ start_server {
     }
 
     test {XRANGE fuzzing} {
+        insert_into_stream_key mystream{t}
+        set items [r XRANGE mystream{t} - +]
         set low_id [lindex $items 0 0]
         set high_id [lindex $items end 0]
         for {set j 0} {$j < 100} {incr j} {
             set start [streamRandomID $low_id $high_id]
             set end [streamRandomID $low_id $high_id]
-            set range [r xrange mystream $start $end]
+            set range [r xrange mystream{t} $start $end]
             set tcl_range [streamSimulateXRANGE $items $start $end]
             if {$range ne $tcl_range} {
                 puts "*** WARNING *** - XRANGE fuzzing mismatch: $start - $end"
@@ -324,7 +545,7 @@ start_server {
         }
     }
 
-    test {XREVRANGE regression test for issue #5006} {
+    test {XREVRANGE regression test for issue 5006} {
         # Add non compressed entries
         r xadd teststream 1234567891230 key1 value1
         r xadd teststream 1234567891240 key2 value2
@@ -353,22 +574,100 @@ start_server {
         r del x
         set rd [redis_deferring_client]
         $rd XREAD BLOCK 0 STREAMS x 1-18446744073709551615
+        # wait_for_blocked_clients_count 1 -> wait_for_blocked_client
+        wait_for_blocked_client
         r XADD x 1-1 f v
         r XADD x 1-18446744073709551615 f v
         r XADD x 2-1 f v
         set res [$rd read]
         assert {[lindex $res 0 1 0] == {2-1 {f v}}}
+        $rd close
     }
 
     test {XADD streamID edge} {
         r del x
-        r XADD x 2577343934890-18446744073709551615 f v ;# we need the timestamp to be in the future
+        r XADD x 2577343934890-18446744073709551615 f v ; # we need the timestamp to be in the future
         r XADD x * f2 v2
         assert_equal [r XRANGE x - +] {{2577343934890-18446744073709551615 {f v}} {2577343934891-0 {f2 v2}}}
     }
+
+    test {XTRIM with MAXLEN option basic test} {
+        r DEL mystream
+        for {set j 0} {$j < 1000} {incr j} {
+            if {rand() < 0.9} {
+                r XADD mystream * xitem $j
+            } else {
+                r XADD mystream * yitem $j
+            }
+        }
+        r XTRIM mystream MAXLEN 666
+        assert {[r XLEN mystream] == 666}
+        r XTRIM mystream MAXLEN = 555
+        assert {[r XLEN mystream] == 555}
+#        r XTRIM mystream MAXLEN ~ 444
+#        assert {[r XLEN mystream] == 500}
+        r XTRIM mystream MAXLEN ~ 400
+        assert {[r XLEN mystream] == 400}
+    }
+
+#    test {XADD with LIMIT consecutive calls} {
+#        r del mystream
+#        r config set stream-node-max-entries 10
+#        for {set j 0} {$j < 100} {incr j} {
+#            r XADD mystream * xitem v
+#        }
+#        r XADD mystream MAXLEN ~ 55 LIMIT 30 * xitem v
+#        assert {[r xlen mystream] == 71}
+#        r XADD mystream MAXLEN ~ 55 LIMIT 30 * xitem v
+#        assert {[r xlen mystream] == 62}
+#        r config set stream-node-max-entries 100
+#    }
+#
+#    test {XTRIM with ~ is limited} {
+#        r del mystream
+#        r config set stream-node-max-entries 1
+#        for {set j 0} {$j < 102} {incr j} {
+#            r XADD mystream * xitem v
+#        }
+#        r XTRIM mystream MAXLEN ~ 1
+#        assert {[r xlen mystream] == 2}
+#        r config set stream-node-max-entries 100
+#    }
+
+    test {XTRIM without ~ is not limited} {
+        r del mystream
+        r config set stream-node-max-entries 1
+        for {set j 0} {$j < 102} {incr j} {
+            r XADD mystream * xitem v
+        }
+        r XTRIM mystream MAXLEN 1
+        assert {[r xlen mystream] == 1}
+        r config set stream-node-max-entries 100
+    }
+
+    test {XTRIM without ~ and with LIMIT} {
+        r del mystream
+        r config set stream-node-max-entries 1
+        for {set j 0} {$j < 102} {incr j} {
+            r XADD mystream * xitem v
+        }
+        assert_error ERR* {r XTRIM mystream MAXLEN 1 LIMIT 30}
+    }
+
+    test {XTRIM with LIMIT delete entries no more than limit} {
+        r del mystream
+        r config set stream-node-max-entries 2
+        for {set j 0} {$j < 3} {incr j} {
+            r XADD mystream * xitem v
+        }
+
+#
+#        assert {[r XTRIM mystream MAXLEN ~ 0 LIMIT 1] == 0}
+        assert {[r XTRIM mystream MAXLEN ~ 0 LIMIT 2] == 2}
+    }
 }
 
-start_server {tags {"stream"} overrides {appendonly yes}} {
+start_server {tags {"stream needs:debug"} overrides {appendonly yes}} {
     test {XADD with MAXLEN > xlen can propagate correctly} {
         for {set j 0} {$j < 100} {incr j} {
             r XADD mystream * xitem v
@@ -376,95 +675,275 @@ start_server {tags {"stream"} overrides {appendonly yes}} {
         r XADD mystream MAXLEN 200 * xitem v
         incr j
         assert {[r xlen mystream] == $j}
-        r debug loadaof
+        # r debug loadaof
         r XADD mystream * xitem v
         incr j
         assert {[r xlen mystream] == $j}
     }
 }
 
-start_server {tags {"stream"} overrides {appendonly yes}} {
-    test {XADD with ~ MAXLEN can propagate correctly} {
+start_server {tags {"stream needs:debug"} overrides {appendonly yes}} {
+    test {XADD with MINID > lastid can propagate correctly} {
         for {set j 0} {$j < 100} {incr j} {
-            r XADD mystream * xitem v
+            set id [expr {$j+1}]
+            r XADD mystream $id xitem v
         }
-        r XADD mystream MAXLEN ~ $j * xitem v
+        r XADD mystream MINID 1 * xitem v
         incr j
         assert {[r xlen mystream] == $j}
-        r config set stream-node-max-entries 1
-        r debug loadaof
+        # r debug loadaof
         r XADD mystream * xitem v
         incr j
         assert {[r xlen mystream] == $j}
     }
 }
 
-start_server {tags {"stream"} overrides {appendonly yes stream-node-max-entries 10}} {
-    test {XTRIM with ~ MAXLEN can propagate correctly} {
-        for {set j 0} {$j < 100} {incr j} {
-            r XADD mystream * xitem v
-        }
-        r XTRIM mystream MAXLEN ~ 85
-        assert {[r xlen mystream] == 90}
-        r config set stream-node-max-entries 1
-        r debug loadaof
-        r XADD mystream * xitem v
-        incr j
-        assert {[r xlen mystream] == 91}
-    }
-}
-
-start_server {tags {"stream xsetid"}} {
-    test {XADD can CREATE an empty stream} {
-        r XADD mystream MAXLEN 0 * a b
-        assert {[dict get [r xinfo stream mystream] length] == 0}
-    }
-
-    test {XSETID can set a specific ID} {
-        r XSETID mystream "200-0"
-        assert {[dict get [r xinfo stream mystream] last-generated-id] == "200-0"}
-    }
-
-    test {XSETID cannot SETID with smaller ID} {
-        r XADD mystream * a b
-        catch {r XSETID mystream "1-1"} err
-        r XADD mystream MAXLEN 0 * a b
-        set err
-    } {ERR*smaller*}
-
-    test {XSETID cannot SETID on non-existent key} {
-        catch {r XSETID stream 1-1} err
-        set _ $err
-    } {ERR no such key}
-}
-
-start_server {tags {"stream"} overrides {appendonly yes aof-use-rdb-preamble no}} {
-    test {Empty stream can be rewrite into AOF correctly} {
-        r XADD mystream MAXLEN 0 * a b
-        assert {[dict get [r xinfo stream mystream] length] == 0}
-        r bgrewriteaof
-        waitForBgrewriteaof r
-        r debug loadaof
-        assert {[dict get [r xinfo stream mystream] length] == 0}
-    }
-
-    test {Stream can be rewrite into AOF correctly after XDEL lastid} {
-        r XSETID mystream 0-0
-        r XADD mystream 1-1 a b
-        r XADD mystream 2-2 a b
-        assert {[dict get [r xinfo stream mystream] length] == 2}
-        r XDEL mystream 2-2
-        r bgrewriteaof
-        waitForBgrewriteaof r
-        r debug loadaof
-        assert {[dict get [r xinfo stream mystream] length] == 1}
-        assert {[dict get [r xinfo stream mystream] last-generated-id] == "2-2"}
-    }
-}
-
+#start_server {tags {"stream needs:debug"} overrides {appendonly yes stream-node-max-entries 100}} {
+#    test {XADD with ~ MAXLEN can propagate correctly} {
+#        for {set j 0} {$j < 100} {incr j} {
+#            r XADD mystream * xitem v
+#        }
+#        r XADD mystream MAXLEN ~ $j * xitem v
+#        incr j
+#        assert {[r xlen mystream] == $j}
+#        r config set stream-node-max-entries 1
+#        r debug loadaof
+#        r XADD mystream * xitem v
+#        incr j
+#        assert {[r xlen mystream] == $j}
+#    }
+#}
+#
+#start_server {tags {"stream needs:debug"} overrides {appendonly yes stream-node-max-entries 10}} {
+#    test {XADD with ~ MAXLEN and LIMIT can propagate correctly} {
+#        for {set j 0} {$j < 100} {incr j} {
+#            r XADD mystream * xitem v
+#        }
+#        r XADD mystream MAXLEN ~ 55 LIMIT 30 * xitem v
+#        assert {[r xlen mystream] == 71}
+#        r config set stream-node-max-entries 1
+#        r debug loadaof
+#        r XADD mystream * xitem v
+#        assert {[r xlen mystream] == 72}
+#    }
+#}
+#
+#start_server {tags {"stream needs:debug"} overrides {appendonly yes stream-node-max-entries 100}} {
+#    test {XADD with ~ MINID can propagate correctly} {
+#        for {set j 0} {$j < 100} {incr j} {
+#            set id [expr {$j+1}]
+#            r XADD mystream $id xitem v
+#        }
+#        r XADD mystream MINID ~ $j * xitem v
+#        incr j
+#        assert {[r xlen mystream] == $j}
+#        r config set stream-node-max-entries 1
+#        r debug loadaof
+#        r XADD mystream * xitem v
+#        incr j
+#        assert {[r xlen mystream] == $j}
+#    }
+#}
+#
+#start_server {tags {"stream needs:debug"} overrides {appendonly yes stream-node-max-entries 10}} {
+#    test {XADD with ~ MINID and LIMIT can propagate correctly} {
+#        for {set j 0} {$j < 100} {incr j} {
+#            set id [expr {$j+1}]
+#            r XADD mystream $id xitem v
+#        }
+#        r XADD mystream MINID ~ 55 LIMIT 30 * xitem v
+#        assert {[r xlen mystream] == 71}
+#        r config set stream-node-max-entries 1
+#        r debug loadaof
+#        r XADD mystream * xitem v
+#        assert {[r xlen mystream] == 72}
+#    }
+#}
+#
+#start_server {tags {"stream needs:debug"} overrides {appendonly yes stream-node-max-entries 10}} {
+#    test {XTRIM with ~ MAXLEN can propagate correctly} {
+#        for {set j 0} {$j < 100} {incr j} {
+#            r XADD mystream * xitem v
+#        }
+#        r XTRIM mystream MAXLEN ~ 85
+#        assert {[r xlen mystream] == 90}
+#        r config set stream-node-max-entries 1
+#        r debug loadaof
+#        r XADD mystream * xitem v
+#        incr j
+#        assert {[r xlen mystream] == 91}
+#    }
+#}
+#
+#start_server {tags {"stream"}} {
+#    test {XADD can CREATE an empty stream} {
+#        r XADD mystream MAXLEN 0 * a b
+#        assert {[dict get [r xinfo stream mystream] length] == 0}
+#    }
+#
+#    test {XSETID can set a specific ID} {
+#        r XSETID mystream "200-0"
+#        set reply [r XINFO stream mystream]
+#        assert_equal [dict get $reply last-generated-id] "200-0"
+#        assert_equal [dict get $reply entries-added] 1
+#    }
+#
+#    test {XSETID cannot SETID with smaller ID} {
+#        r XADD mystream * a b
+#        catch {r XSETID mystream "1-1"} err
+#        r XADD mystream MAXLEN 0 * a b
+#        set err
+#    } {ERR *smaller*}
+#
+#    test {XSETID cannot SETID on non-existent key} {
+#        catch {r XSETID stream 1-1} err
+#        set _ $err
+#    } {ERR no such key}
+#
+#    test {XSETID cannot run with an offset but without a maximal tombstone} {
+#        catch {r XSETID stream 1-1 0} err
+#        set _ $err
+#    } {ERR syntax error}
+#
+#    test {XSETID cannot run with a maximal tombstone but without an offset} {
+#        catch {r XSETID stream 1-1 0-0} err
+#        set _ $err
+#    } {ERR syntax error}
+#
+#    test {XSETID errors on negstive offset} {
+#        catch {r XSETID stream 1-1 ENTRIESADDED -1 MAXDELETEDID 0-0} err
+#        set _ $err
+#    } {ERR *must be positive}
+#
+#    test {XSETID cannot set the maximal tombstone with larger ID} {
+#        r DEL x
+#        r XADD x 1-0 a b
+#
+#        catch {r XSETID x "1-0" ENTRIESADDED 1 MAXDELETEDID "2-0" } err
+#        r XADD mystream MAXLEN 0 * a b
+#        set err
+#    } {ERR *smaller*}
+#
+#    test {XSETID cannot set the offset to less than the length} {
+#        r DEL x
+#        r XADD x 1-0 a b
+#
+#        catch {r XSETID x "1-0" ENTRIESADDED 0 MAXDELETEDID "0-0" } err
+#        r XADD mystream MAXLEN 0 * a b
+#        set err
+#    } {ERR *smaller*}
+#
+#    test {XSETID cannot set smaller ID than current MAXDELETEDID} {
+#        r DEL x
+#        r XADD x 1-1 a 1
+#        r XADD x 1-2 b 2
+#        r XADD x 1-3 c 3
+#        r XDEL x 1-2
+#        r XDEL x 1-3
+#        set reply [r XINFO stream x]
+#        assert_equal [dict get $reply max-deleted-entry-id] "1-3"
+#        catch {r XSETID x "1-2" } err
+#        set err
+#    } {ERR *smaller*}
+#}
+#
 start_server {tags {"stream"}} {
-    test {XGROUP HELP should not have unexpected options} {
-        catch {r XGROUP help xxx} e
-        assert_match "*Unknown subcommand or wrong number of arguments*" $e
+#    test {XADD advances the entries-added counter and sets the recorded-first-entry-id} {
+#        r DEL x
+#        r XADD x 1-0 data a
+#
+#        set reply [r XINFO STREAM x FULL]
+#        assert_equal [dict get $reply entries-added] 1
+#        assert_equal [dict get $reply recorded-first-entry-id] "1-0"
+#
+#        r XADD x 2-0 data a
+#        set reply [r XINFO STREAM x FULL]
+#        assert_equal [dict get $reply entries-added] 2
+#        assert_equal [dict get $reply recorded-first-entry-id] "1-0"
+#    }
+#
+#    test {XDEL/TRIM are reflected by recorded first entry} {
+#        r DEL x
+#        r XADD x 1-0 data a
+#        r XADD x 2-0 data a
+#        r XADD x 3-0 data a
+#        r XADD x 4-0 data a
+#        r XADD x 5-0 data a
+#
+#        set reply [r XINFO STREAM x FULL]
+#        assert_equal [dict get $reply entries-added] 5
+#        assert_equal [dict get $reply recorded-first-entry-id] "1-0"
+#
+#        r XDEL x 2-0
+#        set reply [r XINFO STREAM x FULL]
+#        assert_equal [dict get $reply recorded-first-entry-id] "1-0"
+#
+#        r XDEL x 1-0
+#        set reply [r XINFO STREAM x FULL]
+#        assert_equal [dict get $reply recorded-first-entry-id] "3-0"
+#
+#        r XTRIM x MAXLEN = 2
+#        set reply [r XINFO STREAM x FULL]
+#        assert_equal [dict get $reply recorded-first-entry-id] "4-0"
+#    }
+#
+#    test {Maximum XDEL ID behaves correctly} {
+#        r DEL x
+#        r XADD x 1-0 data a
+#        r XADD x 2-0 data b
+#        r XADD x 3-0 data c
+#
+#        set reply [r XINFO STREAM x FULL]
+#        assert_equal [dict get $reply max-deleted-entry-id] "0-0"
+#
+#        r XDEL x 2-0
+#        set reply [r XINFO STREAM x FULL]
+#        assert_equal [dict get $reply max-deleted-entry-id] "2-0"
+#
+#        r XDEL x 1-0
+#        set reply [r XINFO STREAM x FULL]
+#        assert_equal [dict get $reply max-deleted-entry-id] "2-0"
+#    }
+
+    test {XADD with artial ID with maximal seq} {
+        r DEL x
+        r XADD x 1-18446744073709551615 f1 v1
+        assert_error {*The ID specified in XADD is equal or smaller*} {r XADD x 1-* f2 v2}
     }
 }
+
+#start_server {tags {"stream needs:debug"} overrides {appendonly yes aof-use-rdb-preamble no}} {
+#    test {Empty stream can be rewrite into AOF correctly} {
+#        r XADD mystream MAXLEN 0 * a b
+#        assert {[dict get [r xinfo stream mystream] length] == 0}
+#        r bgrewriteaof
+#        waitForBgrewriteaof r
+#        r debug loadaof
+#        assert {[dict get [r xinfo stream mystream] length] == 0}
+#    }
+#
+#    test {Stream can be rewrite into AOF correctly after XDEL lastid} {
+#        r XSETID mystream 0-0
+#        r XADD mystream 1-1 a b
+#        r XADD mystream 2-2 a b
+#        assert {[dict get [r xinfo stream mystream] length] == 2}
+#        r XDEL mystream 2-2
+#        r bgrewriteaof
+#        waitForBgrewriteaof r
+#        r debug loadaof
+#        assert {[dict get [r xinfo stream mystream] length] == 1}
+#        assert_equal [dict get [r xinfo stream mystream] last-generated-id] "2-2"
+#    }
+#}
+#
+#start_server {tags {"stream"}} {
+#    test {XGROUP HELP should not have unexpected options} {
+#        catch {r XGROUP help xxx} e
+#        assert_match "*wrong number of arguments for 'xgroup|help' command" $e
+#    }
+#
+#    test {XINFO HELP should not have unexpected options} {
+#        catch {r XINFO help xxx} e
+#        assert_match "*wrong number of arguments for 'xinfo|help' command" $e
+#    }
+#}
\ No newline at end of file
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZDiff.java b/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZDiff.java
index d1de5c2..282b9fc 100644
--- a/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZDiff.java
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZDiff.java
@@ -122,7 +122,7 @@ public class TestZDiff {
     }
 
     @TestTemplate
-    public void testZDiffFuzzing(Jedis jedis) {
+    public void testZDiffStress(Jedis jedis) {
         for (int j = 0; j < 100; j++) {
             Map<String, String> s = new HashMap<>();
             List<String> argsList = new ArrayList<>();
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XAddTests.java b/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XAddTests.java
new file mode 100644
index 0000000..bf99e7f
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XAddTests.java
@@ -0,0 +1,169 @@
+package com.github.fppt.jedismock.comparisontests.streams;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.StreamEntryID;
+import redis.clients.jedis.exceptions.JedisDataException;
+import redis.clients.jedis.params.XAddParams;
+
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(ComparisonBase.class)
+public class XAddTests {
+
+    private static final java.util.Map<String, String> HASH = Collections.singletonMap("a", "b");
+
+    @BeforeEach
+    void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+    @TestTemplate
+    void whenAllOptionsUsed_ensureWorksCorrect(Jedis jedis) {
+        jedis.xadd(
+            "s",
+            XAddParams.xAddParams().noMkStream().approximateTrimming().maxLen(3).limit(3).id(1),
+                HASH
+        );
+
+        assertThat(jedis.exists("s")).isFalse();
+
+        jedis.xadd("s", XAddParams.xAddParams().id(1), HASH);
+
+        jedis.xadd(
+                "s",
+                XAddParams.xAddParams().noMkStream().approximateTrimming().minId("0-0").limit(3).id(2),
+                HASH
+        );
+
+        assertThat(jedis.exists("s")).isTrue();
+        assertThat(jedis.xlen("s")).isEqualTo(2);
+    }
+
+    @TestTemplate
+    void whenLimitOptionIsUsedWithoutTilda_ensureThrowsException(Jedis jedis) {
+        jedis.xadd(
+                "s",
+                XAddParams.xAddParams().noMkStream().approximateTrimming().maxLen(3).limit(3).id(1),
+                HASH
+        );
+
+        assertThat(jedis.exists("s")).isFalse();
+
+        jedis.xadd("s", XAddParams.xAddParams().id(1), HASH);
+
+        jedis.xadd(
+                "s",
+                XAddParams.xAddParams().noMkStream().approximateTrimming().minId("0-0").limit(3).id(2),
+                HASH
+        );
+
+        assertThat(jedis.exists("s")).isTrue();
+        assertThat(jedis.xlen("s")).isEqualTo(2);
+    }
+
+    @TestTemplate
+    void whenLimitIsProvided_ensureDoesNotExceed(Jedis jedis) {
+        jedis.xadd("s", new StreamEntryID(0, 1), HASH);
+        jedis.xadd("s", new StreamEntryID(0, 2), HASH);
+        jedis.xadd("s", new StreamEntryID(1, 0), HASH);
+        jedis.xadd("s", new StreamEntryID(1, 1), HASH);
+
+        jedis.xadd(
+                "s",
+                XAddParams.xAddParams().approximateTrimming().minId("2").limit(2),
+                HASH
+        );
+
+        long len = jedis.xlen("s");
+        assertThat(len >= 3).isTrue();
+    }
+
+    @TestTemplate
+    void whenAddedIdIsLowerThanMinId_ensureStreamIsEmpty(Jedis jedis) {
+        jedis.xadd("s", new StreamEntryID(0, 1), HASH);
+        jedis.xadd("s", new StreamEntryID(0, 2), HASH);
+        jedis.xadd("s", new StreamEntryID(1, 0), HASH);
+        jedis.xadd("s", new StreamEntryID(1, 1), HASH);
+
+        jedis.xadd(
+                "s",
+                XAddParams.xAddParams().exactTrimming().minId("2").id("1-2"),
+                HASH
+        );
+
+        assertThat(jedis.xlen("s")).isEqualTo(0);
+    }
+
+    @TestTemplate
+    void whenNomkstream_ensureReturnsNull(Jedis jedis) {
+        assertThat(jedis.xadd(
+                "s",
+                XAddParams.xAddParams().noMkStream().id("*"),
+                HASH
+        )).isNull();
+
+        assertThat(jedis.xadd(
+                "s",
+                XAddParams.xAddParams().minId("1").noMkStream().id("*"),
+                HASH
+        )).isNull();
+
+        assertThat(jedis.xadd(
+                "s",
+                XAddParams.xAddParams().maxLen(1).noMkStream().id("*"),
+                HASH
+        )).isNull();
+
+        assertThat(jedis.xadd(
+                "s",
+                XAddParams.xAddParams().minId("1").exactTrimming().noMkStream().id("*"),
+                HASH
+        )).isNull();
+
+        assertThat(jedis.xadd(
+                "s",
+                XAddParams.xAddParams().maxLen(1).exactTrimming().noMkStream().id("*"),
+                HASH
+        )).isNull();
+
+        assertThat(jedis.xadd(
+                "s",
+                XAddParams.xAddParams().maxLen(1).approximateTrimming().noMkStream().id("*"),
+                HASH
+        )).isNull();
+
+        assertThat(jedis.xadd(
+                "s",
+                XAddParams.xAddParams().minId("1").approximateTrimming().noMkStream().id("*"),
+                HASH
+        )).isNull();
+
+        assertThat(jedis.xadd(
+                "s",
+                XAddParams.xAddParams().minId("1").approximateTrimming().limit(3).noMkStream().id("*"),
+                HASH
+        )).isNull();
+
+        assertThat(jedis.xadd(
+                "s",
+                XAddParams.xAddParams().maxLen(1).approximateTrimming().limit(3).noMkStream().id("*"),
+                HASH
+        )).isNull();
+    }
+
+    @TestTemplate
+    void whenZeroId_ensureThrowsException(Jedis jedis) {
+        assertThatThrownBy(
+                () -> jedis.xadd("s", XAddParams.xAddParams().id("0"), HASH)
+        )
+                .isInstanceOf(JedisDataException.class)
+                .hasMessage("ERR The ID specified in XADD must be greater than 0-0");
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XDelTests.java b/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XDelTests.java
new file mode 100644
index 0000000..c4fc96b
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XDelTests.java
@@ -0,0 +1,84 @@
+package com.github.fppt.jedismock.comparisontests.streams;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.StreamEntryID;
+import redis.clients.jedis.exceptions.JedisDataException;
+import redis.clients.jedis.params.XAddParams;
+
+import java.util.Collections;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(ComparisonBase.class)
+public class XDelTests {
+
+    private static final Map<String, String> HASH = Collections.singletonMap("a", "b");
+
+    @BeforeEach
+    void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+    @TestTemplate
+    void whenTopEntryIsDeleted_ensureTopIdDoesNotDecrease(Jedis jedis) {
+        jedis.xadd("s", XAddParams.xAddParams().id("0-1"), HASH);
+
+        assertThat(jedis.xdel("s", new StreamEntryID(0, 1))).isEqualTo(1);
+
+        assertThatThrownBy(
+                () -> jedis.xadd("s", XAddParams.xAddParams().id("0-1"), HASH)
+        )
+                .isInstanceOf(JedisDataException.class)
+                .hasMessage("ERR The ID specified in XADD is equal or smaller than the target stream top item");
+    }
+
+    @TestTemplate
+    void whenTopEntryIsDeleted_ensureNextEntryIdIsIncremental(Jedis jedis) {
+        jedis.xadd("s", XAddParams.xAddParams().id("2-1"), HASH);
+
+        assertThat(jedis.xdel("s", new StreamEntryID(2, 1))).isEqualTo(1);
+
+        assertThat(
+                jedis.xadd("s", XAddParams.xAddParams().id("2-*"), HASH)
+        ).isEqualTo(new StreamEntryID(2, 2));
+    }
+
+    @TestTemplate
+    void whenAllElementsAreRemoved_ensureStreamIsNotAlsoRemoved(Jedis jedis) {
+        jedis.xadd("s", XAddParams.xAddParams().id("0-1"), HASH);
+        jedis.xadd("s", XAddParams.xAddParams().id("0-2"), HASH);
+        jedis.xadd("s", XAddParams.xAddParams().id("1-0"), HASH);
+        jedis.xadd("s", XAddParams.xAddParams().id("1-1"), HASH);
+
+        assertThat(
+                jedis.xdel(
+                        "s",
+                        new StreamEntryID(0, 1),
+                        new StreamEntryID(0, 2),
+                        new StreamEntryID(1, 0),
+                        new StreamEntryID(1, 1)
+                )
+        ).isEqualTo(4);
+
+        assertThat(jedis.exists("s")).isTrue();
+    }
+
+    @TestTemplate
+    void stressTest(Jedis jedis) {
+        StreamEntryID[] ids = new StreamEntryID[1000];
+
+        for (int i = 0; i < 1000; ++i) {
+            ids[i] = new StreamEntryID(i + 1);
+            jedis.xadd("s", ids[i], HASH);
+        }
+
+        assertThat(jedis.xdel("s", ids)).isEqualTo(1000);
+        assertThat(jedis.xlen("s")).isEqualTo(0);
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XLenTests.java b/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XLenTests.java
new file mode 100644
index 0000000..773eab9
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XLenTests.java
@@ -0,0 +1,63 @@
+package com.github.fppt.jedismock.comparisontests.streams;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.StreamEntryID;
+
+import java.util.Collections;
+import java.util.Random;
+import java.util.StringJoiner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith(ComparisonBase.class)
+public class XLenTests {
+    Random r;
+    @BeforeEach
+    void setUp(Jedis jedis) {
+        jedis.flushAll();
+        r = new Random();
+    }
+
+    String getRandomString(int length) {
+        StringJoiner joiner = new StringJoiner("");
+
+        for (int i = 0; i < length; ++i) {
+            joiner.add(String.valueOf((char)(r.nextInt(26) + 'a')));
+        }
+
+        return joiner.toString();
+    }
+
+    @TestTemplate
+    void whenStreamDoesNotExist_ensureReturnsZero(Jedis jedis) {
+        for (int i = 0; i < 100; ++i) {
+            String key = getRandomString(r.nextInt(21));
+            assertThat(jedis.exists(key)).isFalse();
+            assertThat(jedis.xlen(key)).isEqualTo(0);
+        }
+    }
+
+    @TestTemplate
+    void whenStreamExists_ensureReturnsActualSize(Jedis jedis) {
+        for (int i = 0; i < 100; ++i) {
+            String key = getRandomString(r.nextInt(21));
+            int len = r.nextInt(100);
+
+            for (int j = 0; j < len; ++j) {
+                jedis.xadd(
+                        key,
+                        StreamEntryID.NEW_ENTRY,
+                        Collections.singletonMap(getRandomString(3), getRandomString(3))
+                );
+            }
+
+            assertThat(jedis.xlen(key)).isEqualTo(len);
+
+            jedis.del(key);
+        }
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XReadTests.java b/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XReadTests.java
new file mode 100644
index 0000000..3d5cf2b
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XReadTests.java
@@ -0,0 +1,84 @@
+package com.github.fppt.jedismock.comparisontests.streams;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.StreamEntryID;
+import redis.clients.jedis.exceptions.JedisDataException;
+import redis.clients.jedis.params.XAddParams;
+import redis.clients.jedis.params.XReadParams;
+import redis.clients.jedis.resps.StreamEntry;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ExtendWith(ComparisonBase.class)
+public class XReadTests {
+    private ExecutorService blockingThread;
+    private Jedis blockedClient;
+
+    @BeforeEach
+    public void setUp(Jedis jedis, HostAndPort hostAndPort) {
+        jedis.flushAll();
+        blockedClient = new Jedis(hostAndPort.getHost(), hostAndPort.getPort());
+        blockingThread = Executors.newSingleThreadExecutor();
+    }
+
+    @AfterEach
+    public void tearDown() {
+        blockedClient.close();
+        blockingThread.shutdownNow();
+    }
+
+    @TestTemplate
+    void blockingXREADforStreamThatRanDry(Jedis jedis) throws ExecutionException, InterruptedException {
+        jedis.xadd("s", XAddParams.xAddParams().id("666"), Collections.singletonMap("a", "b"));
+        jedis.xdel("s", new StreamEntryID(666));
+
+        jedis.xread(XReadParams.xReadParams().block(10), Collections.singletonMap("s", new StreamEntryID(665)));
+
+        assertThatThrownBy(
+                () -> jedis.xadd("s", XAddParams.xAddParams().id("665"), Collections.singletonMap("a", "b"))
+        )
+                .isInstanceOf(JedisDataException.class)
+                .hasMessageMatching("ERR.*equal.*smaller.*");
+
+        Future<?> future = blockingThread.submit(() -> {
+            List<Map.Entry<String, List<StreamEntry>>> data = blockedClient.xread(
+                    XReadParams.xReadParams().block(0),
+                    Collections.singletonMap("s", new StreamEntryID(665))
+            );
+
+            assertThat(data)
+                    .hasSize(1)
+                    .first()
+                    .extracting(Map.Entry::getValue)
+                    .asList()
+                    .hasSize(1)
+                    .first()
+                    .usingRecursiveComparison()
+                    .isEqualTo(
+                            new StreamEntry(
+                                    new StreamEntryID(667),
+                                    Collections.singletonMap("a", "b")
+                            )
+                    );
+        });
+
+        jedis.xadd("s", XAddParams.xAddParams().id("667"), Collections.singletonMap("a", "b"));
+
+        future.get();
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XTrimTests.java b/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XTrimTests.java
new file mode 100644
index 0000000..dd42a03
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/streams/XTrimTests.java
@@ -0,0 +1,79 @@
+package com.github.fppt.jedismock.comparisontests.streams;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.StreamEntryID;
+import redis.clients.jedis.params.XAddParams;
+import redis.clients.jedis.params.XTrimParams;
+import redis.clients.jedis.resps.StreamEntry;
+
+import java.util.Collections;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith(ComparisonBase.class)
+public class XTrimTests {
+    private static final Map<String, String> HASH = Collections.singletonMap("a", "b");
+    
+    @BeforeEach
+    void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+
+    @TestTemplate
+    void xtrimXdelAreReflectedByRecordedFirstEntry(Jedis jedis) {
+        jedis.xadd("s", XAddParams.xAddParams().id("1-0"), HASH);
+        jedis.xadd("s", XAddParams.xAddParams().id("2-0"), HASH);
+        jedis.xadd("s", XAddParams.xAddParams().id("3-0"), HASH);
+        jedis.xadd("s", XAddParams.xAddParams().id("4-0"), HASH);
+        jedis.xadd("s", XAddParams.xAddParams().id("5-0"), HASH);
+
+        assertThat(jedis.xlen("s")).isEqualTo(5);
+        assertThat(jedis.xrange("s", StreamEntryID.MINIMUM_ID, StreamEntryID.MAXIMUM_ID, 1))
+                .hasSize(1)
+                .first()
+                .usingRecursiveComparison()
+                .isEqualTo(new StreamEntry(
+                        new StreamEntryID(1, 0),
+                        HASH
+                ));
+
+        jedis.xdel("s", new StreamEntryID(2, 0));
+
+        assertThat(jedis.xrange("s", StreamEntryID.MINIMUM_ID, StreamEntryID.MAXIMUM_ID, 1))
+                .hasSize(1)
+                .first()
+                .usingRecursiveComparison()
+                .isEqualTo(new StreamEntry(
+                        new StreamEntryID(1, 0),
+                        HASH
+                ));
+
+        jedis.xdel("s", new StreamEntryID(1, 0));
+
+        assertThat(jedis.xrange("s", StreamEntryID.MINIMUM_ID, StreamEntryID.MAXIMUM_ID, 1))
+                .hasSize(1)
+                .first()
+                .usingRecursiveComparison()
+                .isEqualTo(new StreamEntry(
+                        new StreamEntryID(3, 0),
+                        HASH
+                ));
+
+        jedis.xtrim("s", XTrimParams.xTrimParams().exactTrimming().maxLen(2));
+
+        assertThat(jedis.xrange("s", StreamEntryID.MINIMUM_ID, StreamEntryID.MAXIMUM_ID, 1))
+                .hasSize(1)
+                .first()
+                .usingRecursiveComparison()
+                .isEqualTo(new StreamEntry(
+                        new StreamEntryID(4, 0),
+                        HASH
+                ));
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/strings/StringOperationsTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/strings/StringOperationsTest.java
index 4e84c8b..dddf47e 100644
--- a/src/test/java/com/github/fppt/jedismock/comparisontests/strings/StringOperationsTest.java
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/strings/StringOperationsTest.java
@@ -7,6 +7,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import redis.clients.jedis.HostAndPort;
 import redis.clients.jedis.Jedis;
 import redis.clients.jedis.exceptions.JedisDataException;
+import redis.clients.jedis.params.SetParams;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -169,6 +170,24 @@ public class StringOperationsTest {
         assertThat(jedis.get("foo".getBytes())).containsExactlyInAnyOrder(msg);
     }
 
+    @TestTemplate
+    public void testSetExInvalidNegativeTime(Jedis jedis) {
+        assertThatThrownBy(() ->
+                jedis.setex("foo", -1, "bar")).isInstanceOf(JedisDataException.class)
+                .hasMessageContaining("ERR invalid expire time");
+        assertThatThrownBy(() ->
+                jedis.psetex("foo", -1, "bar")).isInstanceOf(JedisDataException.class)
+                .hasMessageContaining("ERR invalid expire time");
+    }
+
+    @TestTemplate
+    public void testSetInvalidNegativeTime(Jedis jedis) {
+        assertThatThrownBy(() ->
+                jedis.set("a", "b", SetParams.setParams().ex(-1)))
+                .isInstanceOf(JedisDataException.class)
+                .hasMessageContaining("ERR invalid expire time");
+    }
+
     @TestTemplate
     public void testMsetNonUTF8binary(Jedis jedis) {
         jedis.mset("foo".getBytes(), msg);
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/transactions/TransactionOperationsTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/transactions/TransactionOperationsTest.java
index ef52904..6e76cb2 100644
--- a/src/test/java/com/github/fppt/jedismock/comparisontests/transactions/TransactionOperationsTest.java
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/transactions/TransactionOperationsTest.java
@@ -5,7 +5,9 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.TestTemplate;
 import org.junit.jupiter.api.extension.ExtendWith;
 import redis.clients.jedis.Jedis;
+import redis.clients.jedis.Protocol;
 import redis.clients.jedis.Transaction;
+import redis.clients.jedis.commands.ProtocolCommand;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -32,6 +34,14 @@ public class TransactionOperationsTest {
         assertThat(jedis.llen(key)).isEqualTo(3);
     }
 
+    @TestTemplate
+    public void queuedOperationShouldReplyQueued(Jedis jedis) {
+        assertThat((byte[]) jedis.sendCommand(Protocol.Command.MULTI)).asString().isEqualTo("OK");
+        assertThat((byte[]) jedis.sendCommand(Protocol.Command.SET, "a".getBytes(), "b".getBytes()))
+                .asString().isEqualTo("QUEUED");
+        assertThat((byte[]) jedis.sendCommand(Protocol.Command.DISCARD)).asString().isEqualTo("OK");
+    }
+
     @TestTemplate
     public void whenDiscardIsExecuted_EnsureResultsAreDiscarded(Jedis jedis) {
         String key = "my-list";
diff --git a/src/test/java/com/github/fppt/jedismock/datastructures/streams/RMStreamTests.java b/src/test/java/com/github/fppt/jedismock/datastructures/streams/RMStreamTests.java
new file mode 100644
index 0000000..b2e584d
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/datastructures/streams/RMStreamTests.java
@@ -0,0 +1,161 @@
+package com.github.fppt.jedismock.datastructures.streams;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.exception.WrongStreamKeyException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+
+import static java.lang.Long.toUnsignedString;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class RMStreamTests {
+    RMStream stream;
+
+    @BeforeEach
+    void setup() {
+        stream = new RMStream();
+    }
+
+    @Test
+    void updateLastIdTest() {
+        stream.updateLastId(new StreamId(1, 1));
+        assertThat(stream.getLastId()).isEqualTo(new StreamId(1, 1));
+    }
+
+    @Test
+    void whenLongMaxIsLastId_ensureThrowsExceptionOnAsteriskInput() {
+        stream.updateLastId(new StreamId(-1, -1));
+        assertThatThrownBy(
+                () -> stream.replaceAsterisk(Slice.create("*"))
+        )
+                .isInstanceOf(WrongStreamKeyException.class)
+                .hasMessage("ERR The stream has exhausted the last possible ID, unable to add more items");
+    }
+
+    @Test
+    void whenLongMaxIsSecondPartLastId_ensureIncrementsCorrectlyOnAsteriskInput() {
+        stream.updateLastId(new StreamId(3, -1));
+        assertThatCode(
+                () -> assertThat(stream.replaceAsterisk(Slice.create("*"))).isEqualTo(Slice.create("4-0"))
+        ).doesNotThrowAnyException();
+    }
+
+    @Test
+    void whenInvokeReplaceAsterisk_ensureWorksFineWithOnAsteriskInput() {
+        assertThatCode(
+                () -> assertThat(stream.replaceAsterisk(Slice.create("*"))).isEqualTo(Slice.create("0-1"))
+        ).doesNotThrowAnyException();
+
+        stream.updateLastId(new StreamId(1, 1));
+        assertThatCode(
+                () -> assertThat(stream.replaceAsterisk(Slice.create("*"))).isEqualTo(Slice.create("1-2"))
+        ).doesNotThrowAnyException();
+    }
+
+    @Test
+    void whenLongMaxIsSecondPartLastId_ensureThrowsExceptionOnNumberWithAsteriskInput() {
+        stream.updateLastId(new StreamId(3, -1));
+        assertThatThrownBy(
+                () -> stream.replaceAsterisk(Slice.create("3-*"))
+        )
+                .isInstanceOf(WrongStreamKeyException.class)
+                .hasMessage("ERR The ID specified in XADD is equal or smaller than the target stream top item");
+    }
+
+    @Test
+    void whenGivenNumberIsSmallerThanFirstPartLastId_ensureThrowsExceptionOnNumberWithAsteriskInput() {
+        stream.updateLastId(new StreamId(1, 0));
+        assertThatThrownBy(
+                () -> stream.replaceAsterisk(Slice.create("0-*"))
+        )
+                .isInstanceOf(WrongStreamKeyException.class)
+                .hasMessage("ERR The ID specified in XADD is equal or smaller than the target stream top item");
+    }
+
+    @Test
+    void whenNotANumberWasGivenWithAsterisk_ensureThrowsException() {
+        assertThatThrownBy(
+                () -> stream.replaceAsterisk(Slice.create("e-*"))
+        )
+                .isInstanceOf(WrongStreamKeyException.class)
+                .hasMessage("ERR Invalid stream ID specified as stream command argument");
+    }
+
+    @Test
+    void whenInvokeReplaceAsterisk_ensureWorksFineWithOnNumberWithAsteriskInput() {
+        assertThatCode(
+                () -> assertThat(stream.replaceAsterisk(Slice.create("1-*"))).isEqualTo(Slice.create("1-0"))
+        ).doesNotThrowAnyException();
+
+        stream.updateLastId(new StreamId(1, 3));
+        assertThatCode(
+                () -> assertThat(stream.replaceAsterisk(Slice.create("1-*"))).isEqualTo(Slice.create("1-4"))
+        ).doesNotThrowAnyException();
+    }
+
+    @Test
+    void whenKeyIsCorrect_ensureReplaceAsteriskDoesNotChangeIt() {
+        Slice id = Slice.create("1-1");
+        assertThatCode(
+                () -> assertThat(stream.replaceAsterisk(id)).isSameAs(id) // Reference equality
+        ).doesNotThrowAnyException();
+    }
+
+    @Test
+    void whenKeyIsIncorrect_ensureReplaceAsteriskDoesNotChangeIt() {
+        Slice id = Slice.create("qwerty");
+        assertThatCode(
+                () -> assertThat(stream.replaceAsterisk(id)).isSameAs(id) // Reference equality
+        ).doesNotThrowAnyException();
+    }
+
+    @Test
+    void stressAsteriskTest() {
+        for (int i = 0; i < 1000; ++i) {
+            if (Math.random() >= 0.9) {
+                long second = (long) (Math.random() * Long.MAX_VALUE) * (Math.random() >= 0.5 ? 1 : -1);
+                long first = (long) (Math.random() * Long.MAX_VALUE) * (Math.random() >= 0.5 ? 1 : -1);
+
+                stream.updateLastId(new StreamId(first, second));
+
+                assertThatCode(
+                        () -> assertThat(
+                                stream.replaceAsterisk(Slice.create(toUnsignedString(first) + "-*"))
+                        ).isEqualTo(
+                                Slice.create(toUnsignedString(first) + "-" + toUnsignedString(second + 1))
+                        )
+                ).doesNotThrowAnyException();
+
+                assertThatCode(
+                        () -> assertThat(
+                                stream.replaceAsterisk(Slice.create("*"))
+                        ).isEqualTo(
+                                Slice.create(toUnsignedString(first) + "-" + toUnsignedString(second + 1))
+                        )
+                ).doesNotThrowAnyException();
+            } else {
+                long second = -1;
+                long first = (long) (Math.random() * Long.MAX_VALUE) * (Math.random() >= 0.5 ? 1 : -1);
+
+                stream.updateLastId(new StreamId(first, second));
+
+                assertThatThrownBy(
+                        () -> stream.replaceAsterisk(Slice.create(toUnsignedString(first) + "-*"))
+                )
+                        .isInstanceOf(WrongStreamKeyException.class)
+                        .hasMessage("ERR The ID specified in XADD is equal or smaller than the target stream top item");
+
+                assertThatCode(
+                        () -> assertThat(
+                                stream.replaceAsterisk(Slice.create("*"))
+                        ).isEqualTo(
+                                Slice.create(toUnsignedString(first + 1) + "-0")
+                        )
+                ).doesNotThrowAnyException();
+            }
+        }
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapForwardIteratorTests.java b/src/test/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapForwardIteratorTests.java
new file mode 100644
index 0000000..4ec7b07
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapForwardIteratorTests.java
@@ -0,0 +1,168 @@
+package com.github.fppt.jedismock.datastructures.streams;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class SequencedMapForwardIteratorTests {
+    @Test
+    void createSimpleIterator() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        map.append(2, 3);
+        map.append(3, 4);
+
+        SequencedMapIterator<Integer, Integer> it = map.iterator();
+
+        int iterCount = 0;
+
+        while (it.hasNext()) {
+            ++iterCount;
+            Map.Entry<Integer, Integer> entry = it.next();
+            assertThat(entry.getKey()).isEqualTo(iterCount);
+            assertThat(entry.getValue()).isEqualTo(iterCount + 1);
+        }
+
+        assertThat(iterCount).isEqualTo(3);
+    }
+
+    @Test
+    void createComplexIterator() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        map.append(2, 3);
+        map.append(3, 4);
+        map.append(4, 5);
+        map.append(5, 6);
+
+        SequencedMapIterator<Integer, Integer> it = map.iterator(3);
+
+        int iterCount = 0;
+
+        while (it.hasNext()) {
+            ++iterCount;
+            Map.Entry<Integer, Integer> entry = it.next();
+            assertThat(entry.getKey()).isEqualTo(iterCount + 2);
+            assertThat(entry.getValue()).isEqualTo(iterCount + 3);
+        }
+
+        assertThat(iterCount).isEqualTo(3);
+
+        it = map.iterator(5);
+
+        iterCount = 0;
+
+        while (it.hasNext()) {
+            ++iterCount;
+            Map.Entry<Integer, Integer> entry = it.next();
+            assertThat(entry.getKey()).isEqualTo(iterCount + 4);
+            assertThat(entry.getValue()).isEqualTo(iterCount + 5);
+        }
+
+        assertThat(iterCount).isEqualTo(1);
+
+        it = map.iterator(1);
+
+        iterCount = 0;
+
+        while (it.hasNext()) {
+            ++iterCount;
+            Map.Entry<Integer, Integer> entry = it.next();
+            assertThat(entry.getKey()).isEqualTo(iterCount);
+            assertThat(entry.getValue()).isEqualTo(iterCount + 1);
+        }
+
+        assertThat(iterCount).isEqualTo(5);
+    }
+
+    @Test
+    void emptyMapTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+        SequencedMapIterator<Integer, Integer> it = map.iterator();
+
+        assertThat(it.hasNext()).isFalse();
+        assertThatThrownBy(it::next).isInstanceOf(NoSuchElementException.class);
+    }
+
+    @Test
+    void removeTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        map.append(2, 3);
+        map.append(3, 4);
+        map.append(4, 5);
+        map.append(5, 6);
+
+        SequencedMapIterator<Integer, Integer> it = map.iterator();
+
+        int iterCount = 0;
+
+        it.next();
+        it.next();
+        it.remove();
+
+        while (it.hasNext()) {
+            ++iterCount;
+            Map.Entry<Integer, Integer> entry = it.next();
+            assertThat(entry.getKey()).isEqualTo(iterCount + 2);
+            assertThat(entry.getValue()).isEqualTo(iterCount + 3);
+        }
+
+        assertThat(iterCount).isEqualTo(3);
+        assertThat(map.size()).isEqualTo(4);
+        assertThat(map.get(2)).isNull();
+    }
+
+    @Test
+    void nextWasNotInvokedTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        map.append(2, 3);
+        map.append(3, 4);
+        map.append(4, 5);
+        map.append(5, 6);
+
+        SequencedMapIterator<Integer, Integer> it = map.iterator();
+
+        assertThatThrownBy(it::remove).isInstanceOf(IllegalStateException.class);
+    }
+
+    @Test
+    void removeHeadTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        map.append(2, 3);
+        map.append(3, 4);
+        map.append(4, 5);
+        map.append(5, 6);
+
+        SequencedMapIterator<Integer, Integer> it = map.iterator();
+
+        int iterCount = 0;
+
+        it.next();
+        it.remove();
+
+        assertThat(map.size()).isEqualTo(4);
+        assertThat(map.getHead()).isEqualTo(2);
+        assertThat(map.get(1)).isNull();
+
+        while (it.hasNext()) {
+            ++iterCount;
+            Map.Entry<Integer, Integer> entry = it.next();
+            assertThat(entry.getKey()).isEqualTo(iterCount + 1);
+            assertThat(entry.getValue()).isEqualTo(iterCount + 2);
+        }
+
+        assertThat(iterCount).isEqualTo(4);
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapReverseIteratorTests.java b/src/test/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapReverseIteratorTests.java
new file mode 100644
index 0000000..07f6880
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapReverseIteratorTests.java
@@ -0,0 +1,92 @@
+package com.github.fppt.jedismock.datastructures.streams;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class SequencedMapReverseIteratorTests {
+    @Test
+    void createSimpleIterator() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(3, 4);
+        map.append(2, 3);
+        map.append(1, 2);
+
+        SequencedMapIterator<Integer, Integer> it = map.reverseIterator();
+
+        int iterCount = 0;
+
+        while (it.hasNext()) {
+            ++iterCount;
+            Map.Entry<Integer, Integer> entry = it.next();
+            assertThat(entry.getKey()).isEqualTo(iterCount);
+            assertThat(entry.getValue()).isEqualTo(iterCount + 1);
+        }
+
+        assertThat(iterCount).isEqualTo(3);
+    }
+
+    @Test
+    void createComplexIterator() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(5, 6);
+        map.append(4, 5);
+        map.append(3, 4);
+        map.append(2, 3);
+        map.append(1, 2);
+
+        SequencedMapIterator<Integer, Integer> it = map.reverseIterator(3);
+
+        int iterCount = 0;
+
+        while (it.hasNext()) {
+            ++iterCount;
+            Map.Entry<Integer, Integer> entry = it.next();
+            assertThat(entry.getKey()).isEqualTo(iterCount + 2);
+            assertThat(entry.getValue()).isEqualTo(iterCount + 3);
+        }
+
+        assertThat(iterCount).isEqualTo(3);
+
+        it = map.reverseIterator(5);
+
+        iterCount = 0;
+
+        while (it.hasNext()) {
+            ++iterCount;
+            Map.Entry<Integer, Integer> entry = it.next();
+            assertThat(entry.getKey()).isEqualTo(iterCount + 4);
+            assertThat(entry.getValue()).isEqualTo(iterCount + 5);
+        }
+
+        assertThat(iterCount).isEqualTo(1);
+
+        it = map.reverseIterator(1);
+
+        iterCount = 0;
+
+        while (it.hasNext()) {
+            ++iterCount;
+            Map.Entry<Integer, Integer> entry = it.next();
+            assertThat(entry.getKey()).isEqualTo(iterCount);
+            assertThat(entry.getValue()).isEqualTo(iterCount + 1);
+        }
+
+        assertThat(iterCount).isEqualTo(5);
+    }
+
+    @Test
+    void emptyMapTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+        SequencedMapIterator<Integer, Integer> it = map.reverseIterator();
+
+        assertThat(it.hasNext()).isFalse();
+        assertThatThrownBy(it::next).isInstanceOf(NoSuchElementException.class);
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapTests.java b/src/test/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapTests.java
new file mode 100644
index 0000000..1a1a213
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/datastructures/streams/SequencedMapTests.java
@@ -0,0 +1,489 @@
+package com.github.fppt.jedismock.datastructures.streams;
+
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.stream.IntStream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class SequencedMapTests {
+    @Test
+    void initializeMapTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        assertThat(map.getHead()).isNull();
+        assertThat(map.getTail()).isNull();
+        assertThat(map.size()).isEqualTo(0);
+    }
+
+    @Test
+    void addPairsToMapTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        assertThat(map.getHead()).isEqualTo(1);
+        assertThat(map.getTail()).isEqualTo(1);
+        assertThat(map.size()).isEqualTo(1);
+        assertThat(map.get(1)).isEqualTo(2);
+        assertThat(map.get(0)).isNull();
+    }
+
+    @Test
+    void addSequenceToMapTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        map.append(2, 2);
+        map.append(3, 2);
+        map.append(4, 2);
+
+        map.append(4, 2);
+        map.append(0, 2);
+        map.append(2, 2);
+
+        assertThat(map).hasSize(2);
+    }
+
+    @Test
+    void removePairsFromMapTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+
+        Map.Entry<Integer, Integer> entry = map.remove(0);
+
+        assertThat(entry).isNull();
+        assertThat(map.getHead()).isEqualTo(1);
+        assertThat(map.getTail()).isEqualTo(1);
+        assertThat(map.size()).isEqualTo(1);
+        assertThat(map.get(1)).isEqualTo(2);
+
+        entry = map.remove(1);
+
+        assertThat(entry.getKey()).isEqualTo(1);
+        assertThat(entry.getValue()).isEqualTo(2);
+
+        assertThat(map.getHead()).isNull();
+        assertThat(map.getTail()).isNull();
+        assertThat(map.size()).isEqualTo(0);
+    }
+
+    @Test
+    void getNodeTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        map.append(2, 3);
+        map.append(3, 4);
+
+        assertThatThrownBy(() -> map.getNextKey(0)).isInstanceOf(NullPointerException.class);
+        assertThat(map.getNextKey(1)).isEqualTo(2);
+        assertThat(map.getNextKey(2)).isEqualTo(3);
+        assertThat(map.getNextKey(3)).isNull();
+
+        assertThatThrownBy(() -> map.getPreviousKey(0)).isInstanceOf(NullPointerException.class);
+        assertThat(map.getPreviousKey(1)).isNull();
+        assertThat(map.getPreviousKey(2)).isEqualTo(1);
+        assertThat(map.getPreviousKey(3)).isEqualTo(2);
+    }
+
+    @Test
+    void setNextNodeTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        map.append(2, 3);
+        map.append(3, 4);
+
+        assertThatThrownBy(() -> map.setNextKey(0, 1)).isInstanceOf(NullPointerException.class);
+        assertThatThrownBy(() -> map.setNextKey(1, 0)).isInstanceOf(NullPointerException.class);
+
+        map.setNextKey(1, 3);
+
+        assertThat(map.getNextKey(1)).isEqualTo(3);
+        assertThat(map.getNextKey(2)).isEqualTo(3);
+        assertThat(map.getNextKey(3)).isNull();
+
+        assertThat(map.getPreviousKey(1)).isNull();
+        assertThat(map.getPreviousKey(2)).isEqualTo(1);
+        assertThat(map.getPreviousKey(3)).isEqualTo(2);
+    }
+
+    @Test
+    void setPreviousNodeTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        map.append(2, 3);
+        map.append(3, 4);
+
+        assertThatThrownBy(() -> map.setPreviousKey(0, 1)).isInstanceOf(NullPointerException.class);
+        assertThatThrownBy(() -> map.setPreviousKey(1, 0)).isInstanceOf(NullPointerException.class);
+
+        map.setPreviousKey(3, 1);
+
+        assertThat(map.getNextKey(1)).isEqualTo(2);
+        assertThat(map.getNextKey(2)).isEqualTo(3);
+        assertThat(map.getNextKey(3)).isNull();
+
+        assertThat(map.getPreviousKey(1)).isNull();
+        assertThat(map.getPreviousKey(2)).isEqualTo(1);
+        assertThat(map.getPreviousKey(3)).isEqualTo(1);
+    }
+
+    @Test
+    void addRemoveStressTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(0, 0);
+
+        IntStream.range(1, 1_000_001).forEach(el -> {
+            assertThat(map.getTail()).isEqualTo(el - 1);
+            assertThat(map.getHead()).isEqualTo(0);
+            map.append(el, el * 2);
+            assertThat(map.getTail()).isEqualTo(el);
+            assertThat(map.getHead()).isEqualTo(0);
+
+            assertThat(map.size()).isEqualTo(el + 1);
+        });
+
+        IntStream.range(1, 500_000).forEach(el -> {
+            assertThat(map.getTail()).isEqualTo(1_000_000);
+            assertThat(map.getHead()).isEqualTo(0);
+            map.remove(el);
+            assertThat(map.getTail()).isEqualTo(1_000_000);
+            assertThat(map.getHead()).isEqualTo(0);
+
+            assertThat(map.size()).isEqualTo(1_000_001 - el);
+        });
+
+        IntStream.range(0, 249_999).map(el -> 1_000_000 - el).forEach(el -> {
+            assertThat(map.getTail()).isEqualTo(el);
+            assertThat(map.getHead()).isEqualTo(0);
+            map.remove(el);
+            assertThat(map.getTail()).isEqualTo(el - 1);
+            assertThat(map.getHead()).isEqualTo(0);
+
+            assertThat(map.size()).isEqualTo(el - 499_999);
+        });
+
+        Map.Entry<Integer, Integer> entry = map.remove(0);
+
+        assertThat(entry.getKey()).isEqualTo(0);
+        assertThat(entry.getValue()).isEqualTo(0);
+        assertThat(map.size()).isEqualTo(250_002);
+
+        IntStream.range(500_000, 749_999).forEach(el -> {
+            assertThat(map.getTail()).isEqualTo(750_001);
+            assertThat(map.getHead()).isEqualTo(el);
+
+            Map.Entry<Integer, Integer> localEntry = map.remove(el);
+            assertThat(localEntry.getKey()).isEqualTo(el);
+            assertThat(localEntry.getValue()).isEqualTo(el * 2);
+
+            assertThat(map.getTail()).isEqualTo(750_001);
+            assertThat(map.getHead()).isEqualTo(el + 1);
+
+            assertThat(map.size()).isEqualTo( 750_001 - el);
+        });
+    }
+
+    @Test
+    void removeHeadTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        assertThatCode(map::removeHead).doesNotThrowAnyException();
+        assertThat(map.size()).isEqualTo(0);
+        assertThat(map.getHead()).isNull();
+
+        map.append(0, 0);
+        map.append(1, 0);
+        map.append(2, 0);
+
+        assertThatCode(map::removeHead).doesNotThrowAnyException();
+        assertThat(map.size()).isEqualTo(2);
+        assertThat(map.getHead()).isEqualTo(1);
+
+        assertThatCode(map::removeHead).doesNotThrowAnyException();
+        assertThat(map.size()).isEqualTo(1);
+        assertThat(map.getHead()).isEqualTo(2);
+
+        assertThatCode(map::removeHead).doesNotThrowAnyException();
+        assertThat(map.size()).isEqualTo(0);
+        assertThat(map.getHead()).isNull();
+    }
+
+    @Test
+    void forEachTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(0, 9);
+        map.append(1, 8);
+        map.append(2, 7);
+        map.append(3, 6);
+        map.append(4, 5);
+        map.append(5, 4);
+        map.append(6, 3);
+        map.append(7, 2);
+        map.append(8, 1);
+        map.append(9, 0);
+
+        List<Integer> list = new ArrayList<>();
+
+        map.forEach((key, value) -> list.add(key + value));
+
+        assertThat(list.size()).isEqualTo(map.size());
+
+        for (int el : list) {
+            assertThat(el).isEqualTo(9);
+        }
+    }
+
+    @Test
+    void forEachWithNullActionTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        assertThatThrownBy(
+                () -> map.forEach((BiConsumer<? super Integer, ? super Integer>) null)
+        ).isInstanceOf(NullPointerException.class);
+
+        map.append(0, 9);
+        map.append(1, 8);
+        map.append(2, 7);
+        map.append(3, 6);
+        map.append(4, 5);
+        map.append(5, 4);
+        map.append(6, 3);
+        map.append(7, 2);
+        map.append(8, 1);
+        map.append(9, 0);
+
+        assertThatThrownBy(
+                () -> map.forEach((BiConsumer<? super Integer, ? super Integer>) null)
+        ).isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    void getForwardIteratorWithParamWhenMapIsEmptyTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+        SequencedMapIterator<Integer, Integer> it;
+
+        it = map.iterator(7);
+        assertThat(it.hasNext()).isFalse();
+
+        it = map.iterator(0);
+        assertThat(it.hasNext()).isFalse();
+    }
+
+    @Test
+    void getForwardIteratorWithParamWhenKeyExistsTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        map.append(2, 3);
+        map.append(3, 4);
+        map.append(4, 5);
+        map.append(5, 6);
+
+        SequencedMapIterator<Integer, Integer> it;
+
+        it = map.iterator(3);
+        assertThat(it.hasNext()).isTrue();
+
+        Map.Entry<Integer, Integer> entry = it.next();
+
+        assertThat(entry.getKey()).isEqualTo(3);
+        assertThat(entry.getValue()).isEqualTo(4);
+
+        it = map.iterator(5);
+        assertThat(it.hasNext()).isTrue();
+
+        entry = it.next();
+
+        assertThat(entry.getKey()).isEqualTo(5);
+        assertThat(entry.getValue()).isEqualTo(6);
+
+        it = map.iterator(1);
+        assertThat(it.hasNext()).isTrue();
+
+        entry = it.next();
+
+        assertThat(entry.getKey()).isEqualTo(1);
+        assertThat(entry.getValue()).isEqualTo(2);
+    }
+
+    @Test
+    void getForwardIteratorWithParamWithNullBorderTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        map.append(2, 3);
+        map.append(5, 6);
+        map.append(6, 7);
+        map.append(100, 101);
+
+        assertThatThrownBy(() -> map.iterator(null)).isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    void getForwardIteratorWithParamWhenKeyDoesNotExistTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        map.append(2, 3);
+        map.append(5, 6);
+        map.append(6, 7);
+        map.append(100, 101);
+
+        SequencedMapIterator<Integer, Integer> it;
+
+        it = map.iterator(3);
+        assertThat(it.hasNext()).isTrue();
+
+        Map.Entry<Integer, Integer> entry = it.next();
+
+        assertThat(entry.getKey()).isEqualTo(5);
+        assertThat(entry.getValue()).isEqualTo(6);
+
+        it = map.iterator(7);
+        assertThat(it.hasNext()).isTrue();
+
+        entry = it.next();
+
+        assertThat(entry.getKey()).isEqualTo(100);
+        assertThat(entry.getValue()).isEqualTo(101);
+
+        it = map.iterator(Integer.MIN_VALUE);
+        assertThat(it.hasNext()).isTrue();
+
+        entry = it.next();
+
+        assertThat(entry.getKey()).isEqualTo(1);
+        assertThat(entry.getValue()).isEqualTo(2);
+    }
+
+    @Test
+    void getForwardIteratorWithParamStressTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+        SequencedMapForwardIterator<Integer, Integer> it;
+
+        for (int i = 0; i < 5001; i += 5) {
+            map.append(i, i + 3);
+        }
+
+        for (int i = 0; i < 1000; ++i) {
+            int key = (int) (Math.random() * 5000);
+            int correctKey = key + (key % 5 == 0 ? 0 : 5 - key % 5);
+            it = map.iterator(key);
+            Map.Entry<Integer, Integer> entry = it.next();
+            assertThat(entry.getKey()).isEqualTo(correctKey);
+            assertThat(entry.getValue()).isEqualTo(correctKey + 3);
+        }
+    }
+
+    @Test
+    void getReverseIteratorWithParamWithEmptyMapTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+        SequencedMapIterator<Integer, Integer> it;
+
+        it = map.reverseIterator(7);
+        assertThat(it.hasNext()).isFalse();
+
+        it = map.reverseIterator(0);
+        assertThat(it.hasNext()).isFalse();
+    }
+
+    @Test
+    void getReverseIteratorWithParamWhenKeyExistsTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        map.append(2, 3);
+        map.append(3, 4);
+        map.append(4, 5);
+        map.append(5, 6);
+
+        SequencedMapIterator<Integer, Integer> it;
+
+        it = map.reverseIterator(3);
+        assertThat(it.hasNext()).isTrue();
+
+        Map.Entry<Integer, Integer> entry = it.next();
+
+        assertThat(entry.getKey()).isEqualTo(3);
+        assertThat(entry.getValue()).isEqualTo(4);
+
+        it = map.reverseIterator(5);
+        assertThat(it.hasNext()).isTrue();
+
+        entry = it.next();
+
+        assertThat(entry.getKey()).isEqualTo(5);
+        assertThat(entry.getValue()).isEqualTo(6);
+
+        it = map.reverseIterator(1);
+        assertThat(it.hasNext()).isTrue();
+
+        entry = it.next();
+
+        assertThat(entry.getKey()).isEqualTo(1);
+        assertThat(entry.getValue()).isEqualTo(2);
+    }
+
+    @Test
+    void getReverseIteratorWithParamWithNullBorderTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(100, 101);
+        map.append(5, 6);
+        map.append(6, 7);
+        map.append(2, 3);
+        map.append(1, 2);
+
+        assertThatThrownBy(() -> map.reverseIterator(null)).isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    void getReverseIteratorWithParamWhenKeyDoesNotExistTest() {
+        SequencedMap<Integer, Integer> map = new SequencedMap<>();
+
+        map.append(1, 2);
+        map.append(2, 3);
+        map.append(5, 6);
+        map.append(6, 7);
+        map.append(100, 101);
+
+        SequencedMapIterator<Integer, Integer> it;
+
+        it = map.reverseIterator(3);
+        assertThat(it.hasNext()).isTrue();
+
+        Map.Entry<Integer, Integer> entry = it.next();
+
+        assertThat(entry.getKey()).isEqualTo(2);
+        assertThat(entry.getValue()).isEqualTo(3);
+
+        it = map.reverseIterator(99);
+        assertThat(it.hasNext()).isTrue();
+
+        entry = it.next();
+
+        assertThat(entry.getKey()).isEqualTo(6);
+        assertThat(entry.getValue()).isEqualTo(7);
+
+        it = map.reverseIterator(Integer.MAX_VALUE);
+        assertThat(it.hasNext()).isTrue();
+
+        entry = it.next();
+
+        assertThat(entry.getKey()).isEqualTo(100);
+        assertThat(entry.getValue()).isEqualTo(101);
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/datastructures/streams/StreamIdTests.java b/src/test/java/com/github/fppt/jedismock/datastructures/streams/StreamIdTests.java
new file mode 100644
index 0000000..1642f1c
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/datastructures/streams/StreamIdTests.java
@@ -0,0 +1,217 @@
+package com.github.fppt.jedismock.datastructures.streams;
+
+import com.github.fppt.jedismock.exception.WrongStreamKeyException;
+import nl.jqno.equalsverifier.EqualsVerifier;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static java.lang.Long.toUnsignedString;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class StreamIdTests {
+    @Test
+    void equalsHashCodeTest(){
+        EqualsVerifier.forClass(StreamId.class).verify();
+    }
+
+    @Test
+    void zeroComparisonWithZeroTest() {
+        StreamId zero = new StreamId(0, 0);
+        assertThat(zero.isZero()).isTrue();
+        assertThat(zero).isEqualTo(new StreamId());
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+            "0, 1",
+            "1, 0",
+            "1, 1",
+            "0, -1",
+            "-1, 0",
+            "-1, -1"
+    })
+    void zeroComparisonTest(int first, int second) {
+        StreamId other = new StreamId(first, second);
+        assertThat(other.isZero()).isFalse();
+    }
+
+    @Test
+    void incrementMaxKeyTest() {
+        StreamId maxKey = new StreamId(-1, -1);
+        assertThatThrownBy(
+                maxKey::increment
+        )
+                .isInstanceOf(WrongStreamKeyException.class)
+                .hasMessage("ERR invalid start ID for the interval");
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+            "1234567890, 1234567890, 1234567890, 1234567891",
+            "0, -1, 1, 0",
+            "-1234567891, -1234567891, -1234567891, -1234567890"
+    })
+    void incrementTest(int initialFirst, int initialSecond, int expectedFirst, int expectedSecond) {
+        assertThatCode(() -> assertThat(
+                new StreamId(initialFirst, initialSecond).increment()
+        ).isEqualTo(
+                new StreamId(expectedFirst, expectedSecond)
+        )).doesNotThrowAnyException();
+    }
+
+    @Test
+    void decrementMinKeyTest() {
+        StreamId minKey = new StreamId(0, 0);
+        assertThatThrownBy(
+                minKey::decrement
+        )
+                .isInstanceOf(WrongStreamKeyException.class)
+                .hasMessage("ERR invalid end ID for the interval");
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+            "1234567890, 1234567891, 1234567890, 1234567890",
+            "1, 0, 0, -1",
+            "-1234567891, -1234567890, -1234567891, -1234567891"
+    })
+    void decrementTest(int initialFirst, int initialSecond, int expectedFirst, int expectedSecond) {
+        assertThatCode(() -> assertThat(
+                new StreamId(initialFirst, initialSecond).decrement()
+        ).isEqualTo(
+                new StreamId(expectedFirst, expectedSecond)
+        )).doesNotThrowAnyException();
+    }
+
+    @Test
+    void incrementDecrementStressTest() {
+        for (int i = 0; i < 1000; ++i) {
+            if (Math.random() >= 0.9) {
+                long first = (long) (Math.random() * Long.MAX_VALUE) * (Math.random() >= 0.5 ? 1 : -1);
+                long second = (long) (Math.random() * (Long.MAX_VALUE - 1) + 1) * (Math.random() >= 0.5 ? 1 : -1);
+
+                assertThatCode(
+                        () -> assertThat(
+                                new StreamId(first, second).increment()
+                        ).isEqualTo(
+                                new StreamId(first, second + 1)
+                        )
+                ).doesNotThrowAnyException();
+
+                assertThatCode(
+                        () -> assertThat(
+                                new StreamId(first, second).decrement()
+                        ).isEqualTo(
+                                new StreamId(first, second - 1)
+                        )
+                ).doesNotThrowAnyException();
+            } else {
+                long first = (long) (Math.random() * (Long.MAX_VALUE - 1) + 1) * (Math.random() >= 0.5 ? 1 : -1);
+
+                assertThatCode(
+                        () -> assertThat(
+                                new StreamId(first, -1).increment()
+                        ).isEqualTo(
+                                new StreamId(first + 1, 0)
+                        )
+                ).doesNotThrowAnyException();
+
+                assertThatCode(
+                        () -> assertThat(
+                                new StreamId(first, 0).decrement()
+                        ).isEqualTo(
+                                new StreamId(first - 1, -1)
+                        )
+                ).doesNotThrowAnyException();
+            }
+        }
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+            "-1234567890, -1234567890, '18446744072474983726-18446744072474983726'",
+            "0, 0, '0-0'",
+            "-1, 1234567890, '18446744073709551615-1234567890'",
+            "1234567890, -1, '1234567890-18446744073709551615'"
+    })
+    void toStringStressTest(int first, int second, String expectedToString) {
+        assertThat(new StreamId(first, second).toString()).isEqualTo(expectedToString);
+    }
+    @Test
+    void comparisonTest() {
+        StreamId zero = new StreamId();
+        assertThat(zero.compareTo(new StreamId())).isEqualTo(0);
+
+        for (int i = 0; i < 1000; ++i) {
+            long first = (long) (Math.random() * Long.MAX_VALUE) * (Math.random() >= 0.5 ? 1 : -1);
+            long second = (long) (Math.random() * (Long.MAX_VALUE - 1) + 1) * (Math.random() >= 0.5 ? 1 : -1);
+
+            assertThat(
+                    zero.compareTo(new StreamId(first, second)) < 0
+            ).isTrue();
+        }
+
+        StreamId ones = new StreamId(1, 1);
+        assertThat(ones.compareTo(new StreamId(0, -1)) > 0).isTrue();
+        assertThat(ones.compareTo(new StreamId(1, 0)) > 0).isTrue();
+        assertThat(ones.compareTo(new StreamId(1, 1))).isEqualTo(0);
+        assertThat(ones.compareTo(new StreamId(1, 2)) < 0).isTrue();
+        assertThat(ones.compareTo(new StreamId(2, 0)) < 0).isTrue();
+
+        for (int i = 0; i < 1000; ++i) {
+            long first = (long) (Math.random() * (Long.MAX_VALUE - 1) + 1) * (Math.random() >= 0.5 ? 1 : -1);
+            long second = (long) (Math.random() * (Long.MAX_VALUE - 1) + 1) * (Math.random() >= 0.5 ? 1 : -1);
+
+            StreamId id = new StreamId(first, second);
+
+            assertThat(
+                    id.compareTo(new StreamId(first, second + 1)) < 0
+            ).isTrue();
+            assertThat(
+                    id.compareTo(new StreamId(first + 1, 0)) < 0
+            ).isTrue();
+            assertThat(
+                    id.compareTo(new StreamId(first, second - 1)) > 0
+            ).isTrue();
+            assertThat(
+                    id.compareTo(new StreamId(first - 1, -1)) > 0
+            ).isTrue();
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"a", "0-0-0", "a-0", "0-a"})
+    void constructorInvalidIdsTest(String invalidId) {
+        assertThatThrownBy(() -> new StreamId(invalidId))
+                .isInstanceOf(WrongStreamKeyException.class)
+                .hasMessage("ERR Invalid stream ID specified as stream command argument");
+    }
+
+    @Test
+    void constructorStressTest() {
+        for (int i = 0; i < 1000; ++i) {
+            long first = (long) (Math.random() * (Long.MAX_VALUE - 1) + 1) * (Math.random() >= 0.5 ? 1 : -1);
+            long second = (long) (Math.random() * (Long.MAX_VALUE - 1) + 1) * (Math.random() >= 0.5 ? 1 : -1);
+
+            assertThatCode(() -> assertThat(
+                    new StreamId(toUnsignedString(first) + "-" + toUnsignedString(second))
+            ).isEqualTo(
+                    new StreamId(first, second)
+            )).doesNotThrowAnyException();
+        }
+
+        for (int i = 0; i < 1000; ++i) {
+            long first = (long) (Math.random() * (Long.MAX_VALUE - 1) + 1) * (Math.random() >= 0.5 ? 1 : -1);
+
+            assertThatCode(() -> assertThat(
+                    new StreamId(toUnsignedString(first))
+            ).isEqualTo(
+                    new StreamId(first, 0)
+            )).doesNotThrowAnyException();
+        }
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/operations/streams/ValidationTests.java b/src/test/java/com/github/fppt/jedismock/operations/streams/ValidationTests.java
new file mode 100644
index 0000000..53149d4
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/operations/streams/ValidationTests.java
@@ -0,0 +1,133 @@
+package com.github.fppt.jedismock.operations.streams;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.datastructures.streams.RMStream;
+import com.github.fppt.jedismock.datastructures.streams.StreamId;
+import com.github.fppt.jedismock.exception.WrongStreamKeyException;
+import com.github.fppt.jedismock.storage.RedisBase;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class ValidationTests {
+    static RMStream stream = new RMStream();
+    static XAdd xAdd;
+
+    @BeforeAll
+    static void setUp() {
+        RedisBase mock = Mockito.mock(RedisBase.class);
+        Mockito.when(mock.getStream(Mockito.any())).thenReturn(stream);
+        xAdd = new XAdd(mock, Collections.singletonList(Slice.create("mock")));
+    }
+
+    @Test
+    void zeroComparisonWithZeroTest() {
+        StreamId zero = new StreamId(0, 0);
+        assertThrows(
+                WrongStreamKeyException.class,
+                () -> xAdd.validate(zero),
+                "ERR The ID specified in XADD must be greater than 0-0"
+        );
+    }
+
+    @Test
+    void zeroComparisonWithPositiveKeysTest() {
+        StreamId other = new StreamId(0, 1);
+        assertDoesNotThrow(() -> xAdd.validate(other));
+
+        StreamId newOther = new StreamId(1, 0);
+        assertDoesNotThrow(() -> xAdd.validate(newOther));
+
+        StreamId newestOther = new StreamId(1, 1);
+        assertDoesNotThrow(() -> xAdd.validate(newestOther));
+    }
+
+    @Test
+    void zeroComparisonWithNegativeKeysTest() {
+        StreamId other = new StreamId(0, -1);
+        assertDoesNotThrow(() -> xAdd.validate(other));
+
+        StreamId newOther = new StreamId(-1, 0);
+        assertDoesNotThrow(() -> xAdd.validate(newOther));
+
+        StreamId newestOther = new StreamId(-1, -1);
+        assertDoesNotThrow(() -> xAdd.validate(newestOther));
+    }
+
+    @Test
+    void compareTopIdTest() {
+        RMStream stream = new RMStream();
+
+        RedisBase mock = Mockito.mock(RedisBase.class);
+        Mockito.when(mock.getStream(Mockito.any())).thenReturn(stream);
+
+        XAdd xAdd = new XAdd(mock, Collections.singletonList(Slice.create("mock")));
+
+        stream.updateLastId(new StreamId(3, 3));
+
+        assertThrows(
+                WrongStreamKeyException.class,
+                () -> xAdd.validate(new StreamId(3, 3)),
+                "ERR The ID specified in XADD is equal or smaller than the target stream top item"
+        );
+
+        assertThrows(
+                WrongStreamKeyException.class,
+                () -> xAdd.validate(new StreamId(3, 2)),
+                "ERR The ID specified in XADD is equal or smaller than the target stream top item"
+        );
+
+        assertThrows(
+                WrongStreamKeyException.class,
+                () -> xAdd.validate(new StreamId(2, -1)),
+                "ERR The ID specified in XADD is equal or smaller than the target stream top item"
+        );
+
+        StreamId key = new StreamId(3, 4);
+        assertDoesNotThrow(() -> xAdd.validate(key));
+        StreamId newKey = new StreamId(4, 0);
+        assertDoesNotThrow(() -> xAdd.validate(newKey));
+    }
+
+    @Test
+    void stressTest() {
+        RMStream stream = new RMStream();
+
+        RedisBase mock = Mockito.mock(RedisBase.class);
+        Mockito.when(mock.getStream(Mockito.any())).thenReturn(stream);
+
+        XAdd xAdd = new XAdd(mock, Collections.singletonList(Slice.create("mock")));
+
+        for (int i = 0; i < 1000; ++i) {
+            long first = (long) (Math.random() * (Long.MAX_VALUE - 1) + 1) * (Math.random() >= 0.5 ? 1 : -1);
+            long second = (long) (Math.random() * (Long.MAX_VALUE - 1) + 1) * (Math.random() >= 0.5 ? 1 : -1);
+
+            StreamId id = new StreamId(first, second);
+
+            stream.updateLastId(id);
+
+            StreamId key = new StreamId(first, second + 1);
+            assertDoesNotThrow(() -> xAdd.validate(key));
+
+            StreamId newKey = new StreamId(first + 1, 0);
+            assertDoesNotThrow(() -> xAdd.validate(newKey));
+
+            assertThrows(
+                    WrongStreamKeyException.class,
+                    () -> xAdd.validate(new StreamId(first, second - 1)),
+                    "ERR The ID specified in XADD is equal or smaller than the target stream top item"
+            );
+
+            assertThrows(
+                    WrongStreamKeyException.class,
+                    () -> xAdd.validate(new StreamId(first - 1, -1)),
+                    "ERR The ID specified in XADD is equal or smaller than the target stream top item"
+            );
+        }
+    }
+}