diff --git a/SafeExamBrowser.Configuration.UnitTests/SafeExamBrowser.Configuration.UnitTests.csproj b/SafeExamBrowser.Configuration.UnitTests/SafeExamBrowser.Configuration.UnitTests.csproj
index 4d9e254a..de04bb25 100644
--- a/SafeExamBrowser.Configuration.UnitTests/SafeExamBrowser.Configuration.UnitTests.csproj
+++ b/SafeExamBrowser.Configuration.UnitTests/SafeExamBrowser.Configuration.UnitTests.csproj
@@ -96,6 +96,7 @@
+
diff --git a/SafeExamBrowser.Configuration.UnitTests/SubStreamTests.cs b/SafeExamBrowser.Configuration.UnitTests/SubStreamTests.cs
new file mode 100644
index 00000000..6e6b83f8
--- /dev/null
+++ b/SafeExamBrowser.Configuration.UnitTests/SubStreamTests.cs
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+using System;
+using System.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace SafeExamBrowser.Configuration.UnitTests
+{
+ [TestClass]
+ public class SubStreamTests
+ {
+ private Mock stream;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ stream = new Mock();
+
+ stream.SetupGet(s => s.CanRead).Returns(true);
+ stream.SetupGet(s => s.CanSeek).Returns(true);
+ stream.SetupGet(s => s.Length).Returns(1000);
+ }
+
+ [TestMethod]
+ public void MustSetPropertiesCorrectly()
+ {
+ var sut = new SubStream(stream.Object, 100, 200);
+
+ Assert.IsTrue(sut.CanRead);
+ Assert.IsTrue(sut.CanSeek);
+ Assert.IsFalse(sut.CanWrite);
+ Assert.AreEqual(200, sut.Length);
+ Assert.AreEqual(0, sut.Position);
+ }
+
+ [TestMethod]
+ public void MustReadCorrectly()
+ {
+ var position = 750L;
+ var sut = new SubStream(stream.Object, 100, 200);
+
+ stream.SetupGet(s => s.Position).Returns(position);
+ stream.SetupSet(s => s.Position = It.IsAny()).Callback(p => position = p);
+ stream.Setup(s => s.Read(It.IsAny(), It.IsAny(), It.IsAny())).Returns((a, o, c) => c);
+ sut.Position = 50;
+
+ var bytesRead = sut.Read(new byte[25], 0, 25);
+
+ stream.Verify(s => s.Read(It.IsAny(), 0, 25), Times.Once);
+
+ Assert.AreEqual(25, bytesRead);
+ Assert.AreEqual(75, sut.Position);
+ Assert.AreEqual(750, position);
+
+ sut.Position = 150;
+ bytesRead = sut.Read(new byte[75], 0, 75);
+
+ stream.Verify(s => s.Read(It.IsAny(), 0, 50), Times.Once);
+
+ Assert.AreEqual(50, bytesRead);
+ Assert.AreEqual(200, sut.Position);
+ Assert.AreEqual(750, position);
+ }
+
+ [TestMethod]
+ public void MustNotReadOutsideOfBounds()
+ {
+ var sut = new SubStream(stream.Object, 100, 200);
+
+ sut.Position = -1;
+
+ var bytesRead = sut.Read(new byte[0], 0, 0);
+
+ Assert.AreEqual(0, bytesRead);
+ stream.Verify(s => s.ReadByte(), Times.Never);
+ stream.Verify(s => s.Read(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
+
+ sut.Position = 500;
+ bytesRead = sut.Read(new byte[0], 0, 0);
+
+ Assert.AreEqual(0, bytesRead);
+ stream.Verify(s => s.ReadByte(), Times.Never);
+ stream.Verify(s => s.Read(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
+ }
+
+ [TestMethod]
+ public void MustReadByteCorrectly()
+ {
+ var sut = new SubStream(stream.Object, 100, 200);
+
+ stream.SetupGet(s => s.Position).Returns(-100);
+ Assert.AreEqual(-1, sut.ReadByte());
+
+ stream.SetupGet(s => s.Position).Returns(200);
+ Assert.AreEqual(-1, sut.ReadByte());
+
+ stream.SetupGet(s => s.Position).Returns(25);
+ sut.ReadByte();
+ stream.Verify(s => s.Read(It.IsAny(), It.IsAny(), It.IsAny()), Times.AtLeastOnce);
+ }
+
+ [TestMethod]
+ public void MustSeekCorrectly()
+ {
+ var sut = new SubStream(stream.Object, 100, 200);
+
+ sut.Seek(10, SeekOrigin.Begin);
+ Assert.AreEqual(10, sut.Position);
+
+ sut.Seek(15, SeekOrigin.Current);
+ Assert.AreEqual(25, sut.Position);
+
+ sut.Seek(-5, SeekOrigin.Current);
+ Assert.AreEqual(20, sut.Position);
+
+ sut.Seek(-50, SeekOrigin.End);
+ Assert.AreEqual(150, sut.Position);
+
+ sut.Seek(10, SeekOrigin.End);
+ Assert.AreEqual(210, sut.Position);
+
+ sut.Seek(-10, SeekOrigin.Begin);
+ Assert.AreEqual(-10, sut.Position);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void MustNotAllowNonReadableStream()
+ {
+ stream.SetupGet(s => s.CanRead).Returns(false);
+
+ new SubStream(stream.Object, 0, 0);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void MustNotAllowNonSeekableStream()
+ {
+ stream.SetupGet(s => s.CanSeek).Returns(false);
+
+ new SubStream(stream.Object, 0, 0);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void MustNotAllowOffsetSmallerThanZero()
+ {
+ new SubStream(stream.Object, -1, 100);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void MustNotAllowLengthSmallerThanOne()
+ {
+ new SubStream(stream.Object, 100, 0);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(NotSupportedException))]
+ public void MustNotSupportFlushing()
+ {
+ new SubStream(stream.Object, 100, 100).Flush();
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(NotSupportedException))]
+ public void MustNotSupportChangingLength()
+ {
+ new SubStream(stream.Object, 100, 100).SetLength(100);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(NotSupportedException))]
+ public void MustNotSupportWriting()
+ {
+ new SubStream(stream.Object, 100, 100).Write(new byte[0], 0, 0);
+ }
+ }
+}
diff --git a/SafeExamBrowser.Configuration/SubStream.cs b/SafeExamBrowser.Configuration/SubStream.cs
index 3cdda622..20e947d4 100644
--- a/SafeExamBrowser.Configuration/SubStream.cs
+++ b/SafeExamBrowser.Configuration/SubStream.cs
@@ -13,7 +13,6 @@ namespace SafeExamBrowser.Configuration
{
///
/// A read-only wrapper for a subsection of another, larger stream.
- /// TODO: Unit Test!
///
internal class SubStream : Stream
{