diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5ff105c..e7ef771 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,21 +9,22 @@ jobs: timeout-minutes: 10 steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: - node-version: 14 + node-version-file: '.nvmrc' + cache: 'npm' - name: Semantic Release id: release - uses: cycjimmy/semantic-release-action@v2 + uses: cycjimmy/semantic-release-action@v3 env: GITHUB_TOKEN: ${{ github.token }} NPM_TOKEN: ${{ secrets.OSS_NPM_TOKEN }} with: extra_plugins: | - @semantic-release/changelog@5.0.1 - @semantic-release/git@9.0.0 + @semantic-release/changelog@6 + @semantic-release/git@10 - name: Teams notification uses: toko-bifrost/ms-teams-deploy-card@3.1.2 if: always() diff --git a/src/components/CacheFS.brs b/src/components/CacheFS.brs new file mode 100644 index 0000000..fc87894 --- /dev/null +++ b/src/components/CacheFS.brs @@ -0,0 +1,63 @@ +' @import /components/rokuComponents/EVPDigest.brs +function CacheFS() as Object + prototype = {} + + prototype._PREFIX = "cachefs:/" + + prototype._digest = Invalid + prototype._fileSystem = Invalid + + _constructor = function (m as Object) as Object + m._digest = EVPDigest() + m._digest.setup("sha1") + m._fileSystem = CreateObject("roFileSystem") + + return m + end function + + ' Reads data stored in cachefs under the specific key + ' @param {String} key + ' @returns {Object} Invalid if key is incorrect or is not stored + prototype.read = function (key as String) as Object + if (key = "") then return Invalid + + filePath = m._PREFIX + m._hash(key) + if (NOT m._fileSystem.exists(filePath)) then return Invalid + + content = ReadAsciiFile(filePath) + + return ParseJson(content) + end function + + ' Writes data into cachefs under the specific key + ' @param {String} key + ' @param {Object} data - any value acceptable by native FormatJson function except Invalid + ' @returns {Boolean} false if data is not parseable, Invalid or if storing failed + prototype.write = function (key as String, data as Object) as Boolean + if (key = "" OR data = Invalid) then return false + + content = FormatJson(data) + if (content = "") then return false + + return WriteAsciiFile(m._PREFIX + m._hash(key), content) + end function + + ' Deletes data from cachefs under the specific key + ' @param {String} key + ' @returns {Boolean} true if data successfully removed + prototype.delete = function (key as String) as Boolean + if (key = "") then return false + + return DeleteFile(m._PREFIX + m._hash(key)) + end function + + ' @private + prototype._hash = function (text as String) as String + byteArray = CreateObject("roByteArray") + byteArray.fromAsciiString(text) + + return m._digest.process(byteArray) + end function + + return _constructor(prototype) +end function diff --git a/src/components/_tests/CacheFS.test.brs b/src/components/_tests/CacheFS.test.brs new file mode 100644 index 0000000..a5a3986 --- /dev/null +++ b/src/components/_tests/CacheFS.test.brs @@ -0,0 +1,135 @@ +' @import /components/KopytkoTestSuite.brs from @dazn/kopytko-unit-testing-framework +' @mock /components/rokuComponents/EVPDigest.brs +function TestSuite__CacheFS() as Object + ts = KopytkoTestSuite() + ts.name = "CacheFS" + + ts.setBeforeEach(sub (ts as Object) + m.__mocks = {} + m.__mocks.EVPDigest = { + process: { + returnValue: "stringValue", + }, + } + + ts.__dataKey = "example key" + end sub) + + ts.setAfterEach(sub (ts as Object) + CacheFS().delete(ts.__dataKey) + end sub) + + ts.addTest("constructor creates EVPDigest and sets sha1 algorithm", function (ts as Object) as String + ' When + CacheFS() + + ' Then + return ts.assertMethodWasCalled("EVPDigest.setup", { digestType: "sha1" }) + end function) + + ts.addParameterizedTests([ + { super: "data" }, + [1, 2, 3], + "", + 777, + ], "read returns data stored via write method", function (ts as Object, data as Object) as String + ' Given + cache = CacheFS() + if (NOT cache.write(ts.__dataKey, data)) + return ts.fail("Couldn't write data to CacheFS") + end if + + ' When + returnedData = cache.read(ts.__dataKey) + + ' Then + return ts.assertEqual(returnedData, data) + end function) + + ts.addTest("read returns invalid in case of empty data under the given key", function (ts as Object) as String + ' Given + cache = CacheFS() + + ' When + returnedData = cache.read("any key") + + ' Then + return ts.assertInvalid(returnedData) + end function) + + ts.addTest("read returns invalid in case of empty key passed", function (ts as Object) as String + ' Given + cache = CacheFS() + + ' When + returnedData = cache.read("") + + ' Then + return ts.assertInvalid(returnedData) + end function) + + ts.addTest("write returns false in case of empty key passed", function (ts as Object) as String + ' Given + cache = CacheFS() + + ' When + returnedData = cache.write("", { any: "data" }) + + ' Then + return ts.assertFalse(returnedData) + end function) + + ts.addTest("write returns false in case of invalid data passed", function (ts as Object) as String + ' Given + cache = CacheFS() + + ' When + returnedData = cache.write(ts.__dataKey, Invalid) + + ' Then + return ts.assertFalse(returnedData) + end function) + + ts.addTest("write returns false in case of non-parseable data passed", function (ts as Object) as String + ' Given + cache = CacheFS() + + ' When + returnedData = cache.write(ts.__dataKey, CreateObject("roDateTime")) + + ' Then + return ts.assertFalse(returnedData) + end function) + + ts.addTest("delete deletes data stored under the given key", function (ts as Object) as String + ' Given + data = { super: "data" } + + cache = CacheFS() + if (NOT cache.write(ts.__dataKey, data)) + return ts.fail("Couldn't write data to CacheFS") + end if + + ' When + if (NOT cache.delete(ts.__dataKey)) + return ts.fail("Couldn't delete data from CacheFS") + end if + returnedData = cache.read(ts.__dataKey) + + ' Then + return ts.assertInvalid(returnedData) + end function) + + ts.addTest("delete returns false in case of empty key passed", function (ts as Object) as String + ' Given + cache = CacheFS() + + ' When + result = cache.delete("") + + ' Then + return ts.assertFalse(result) + end function) + + return ts +end function diff --git a/src/components/rokuComponents/EVPDigest.brs b/src/components/rokuComponents/EVPDigest.brs new file mode 100644 index 0000000..3af2a87 --- /dev/null +++ b/src/components/rokuComponents/EVPDigest.brs @@ -0,0 +1,5 @@ +' Wrapper function for creating native roDateTime component. +' @class +function EVPDigest() as Object + return CreateObject("roEVPDigest") +end function diff --git a/src/components/rokuComponents/_mocks/EVPDigest.mock.brs b/src/components/rokuComponents/_mocks/EVPDigest.mock.brs new file mode 100644 index 0000000..5f0eccc --- /dev/null +++ b/src/components/rokuComponents/_mocks/EVPDigest.mock.brs @@ -0,0 +1,26 @@ +' @import /components/_mocks/Mock.brs from @dazn/kopytko-unit-testing-framework + +' @returns {Mock} +function EVPDigest() as Object + return Mock({ + testComponent: m, + name: "EVPDigest", + methods: { + setup: function (digestType as String) as Integer + return m.setupMock("setup", { digestType: digestType }, "Integer") + end function, + reinit: function () as Integer + return m.reinitMock("reinit", {}, "Integer") + end function, + process: function (bytes as Object) as String + return m.processMock("process", { bytes: bytes }, "String") + end function, + update: sub (bytes as Object) + m.updateMock("update", { bytes: bytes }) + end sub, + final: function () as String + return m.finalMock("final", {}, "String") + end function, + }, + }) +end function