This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_cryo"]["cryo"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_lumberyak-c9fb3068-76fee23f"]["lumberyak"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["jtaylor_mock"]["mock"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["lua-promise"]["lua-promise"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_roact"]["roact"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_lua-symbol"]["lua-symbol"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,14 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "roblox/lua-roact-policy-provider"
version = "0.1.0"
commit = "5f782af8bb981d6e9d2b3fa92f69b02bd4f4cf37"
source = "git+https://github.com/roblox/lua-roact-policy-provider#master"
dependencies = [
"Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo",
"Lumberyak roblox/lumberyak 76fee23f git+https://github.com/roblox/lumberyak#master",
"Mock jtaylor/mock d2c4005c git+https://github.com/roblox/mock#master",
"Promise lua-promise 1bb842c3 git+https://github.com/roblox/lua-promise#master",
"Roact roblox/roact 1.3.1 url+https://github.com/roblox/roact",
"Symbol roblox/lua-symbol 139fdfe6 git+https://github.com/roblox/lua-symbol#master",
"tutils tutils 937da4f7 git+https://github.com/roblox/tutils#master",
]
@@ -0,0 +1,6 @@
local Packages = script.Parent.Parent
local Lumberyak = require(Packages.Lumberyak)
local logger = Lumberyak.Logger.new()
return logger
@@ -0,0 +1,23 @@
local Packages = script.Parent.Parent
local Roact = require(Packages.Roact)
local appPolicyKey = require(script.Parent.appPolicyKey)
return function()
local PolicyProvider = Roact.Component:extend("PolicyProvider")
function PolicyProvider:init(props)
assert(type(props.policy) == "table", "Provider expects props.policy to be a table")
self._context[appPolicyKey] = {
presentationPolicy = props.policy,
staticExternalPolicy = props.policyData,
}
end
function PolicyProvider:render()
return Roact.oneChild(self.props[Roact.Children])
end
return PolicyProvider
end
@@ -0,0 +1,4 @@
local Packages = script.Parent.Parent
local Symbol = require(Packages.Symbol)
return Symbol.named("AppPolicy")
@@ -0,0 +1,9 @@
return function()
local appPolicyKey = require(script.Parent.appPolicyKey)
describe("require return value", function()
it("SHOULD return a valid Symbol", function()
expect(appPolicyKey).to.be.ok()
end)
end)
end
@@ -0,0 +1,99 @@
local Packages = script.Parent.Parent
local Roact = require(Packages.Roact)
local Cryo = require(Packages.Cryo)
local Promise = require(Packages.Promise)
local Logger = require(script.Parent.Logger)
local appPolicyKey = require(script.Parent.appPolicyKey)
local function mergePolicies(base, params)
local policyWrapper = {}
for _, wrapper in ipairs(params) do
policyWrapper = Cryo.Dictionary.join(policyWrapper, wrapper(base))
end
return policyWrapper
end
return function(getPolicyImpl)
assert(getPolicyImpl, "expected getPolicyImpl")
return function(mapper)
assert(type(mapper) == "function", "connect expects mapper to be a function")
return function(component)
local name = ("AppPolicy(%s)"):format(tostring(component))
local componentLogger = Logger:new(name)
componentLogger:setContext({
prefix = string.format("%s: ", name),
})
local providerNotFound = string.format("%s: Not a descendent of PolicyProvider", name)
local Connection = Roact.PureComponent:extend(name)
componentLogger:trace("Connected to component: {}", tostring(component))
function Connection:init(props)
self.policyContext = self._context[appPolicyKey]
assert(self.policyContext, providerNotFound)
self.setWithEmptyPolicy = function()
self.state = {
policy = mergePolicies({}, self.policyContext.presentationPolicy),
}
end
if self.policyContext.staticExternalPolicy then
-- if we already have a staticExternalPolicy, there is
-- no need to read it from our implementation
self.state = {
policy = mergePolicies(self.policyContext.staticExternalPolicy, self.policyContext.presentationPolicy),
}
else
local retrievedExternalPolicy = getPolicyImpl.read()
if retrievedExternalPolicy then
self.state = {
policy = mergePolicies(retrievedExternalPolicy, self.policyContext.presentationPolicy),
}
else
self.setWithEmptyPolicy()
componentLogger:trace("No app policy data available")
end
self.onPolicyChanged = function(newExternalPolicy)
self:setState({
policy = mergePolicies(newExternalPolicy, self.policyContext.presentationPolicy),
})
end
end
end
function Connection:didMount()
if self._context[appPolicyKey].staticExternalPolicy then
return
end
self.connection = getPolicyImpl.onPolicyChanged(function(incomingExternalPolicy)
componentLogger:trace("Received policy update from MemStorageService")
self.onPolicyChanged(incomingExternalPolicy)
end)
end
function Connection:render()
local policyProps = mapper(self.state.policy, self.props)
local newProps = Cryo.Dictionary.join(self.props, policyProps)
return Roact.createElement(component, newProps)
end
function Connection:willUnmount()
if self.connection then
self.connection:Disconnect()
end
-- sometimes the callback will fire even after :Disconnect was called
self.onPolicyChange = nil
end
return Connection
end
end
end
@@ -0,0 +1,542 @@
return function()
local Packages = script.Parent.Parent
local Roact = require(Packages.Roact)
local tutils = require(Packages.tutils)
local Mock = require(Packages.Mock)
local MagicMock = Mock.MagicMock
local fromMemStorageService = require(script.Parent.getPolicyImplementations.fromMemStorageService)
local Provider = require(script.Parent.Provider)
local providerInstance = Provider()
local function checkForPropsAfterMounting(params)
assert(params.expectedProps)
assert(params.mapper)
assert(params.policyProp)
assert(params.connect)
assert(params.provider)
params.shouldCheckPropsIf = params.shouldCheckPropsIf or function()
return true
end
local hasBaseComponentEverRendered = false
local hasPropsEverBeenChecked = false
local baseComponent = function(props)
hasBaseComponentEverRendered = true
if params.shouldCheckPropsIf() then
hasPropsEverBeenChecked = true
for propName, propValue in pairs(params.expectedProps) do
if props[propName] ~= propValue then
fail(string.format(
"Expected baseComponent to have prop `%s` = `%s`." .. " " ..
"Got: `%s` instead." .. " " ..
"(Check the `mapper` function is correctly formatted)",
propName,
tostring(propValue),
tostring(props[propName])
))
end
end
end
return Roact.createElement("Folder")
end
local wrappedComponent = params.connect(params.mapper)(baseComponent)
local tree = Roact.createElement(params.provider, {
policy = params.policyProp,
policyData = params.policyDataProp,
}, {
wrappedComponent = Roact.createElement(wrappedComponent)
})
local instance = Roact.mount(tree)
if params.funcAfterMounting then
params.funcAfterMounting()
end
Roact.unmount(instance)
expect(hasBaseComponentEverRendered).to.equal(true)
expect(hasPropsEverBeenChecked).to.equal(true)
end
describe("WHEN required", function()
local connect = require(script.Parent.connect)
it("SHOULD return a function", function()
expect(connect).to.be.a("function")
end)
describe("GIVEN a fromMemStorageServiceWithBehavior", function()
local behavior = "mockBehavior"
describe("GIVEN empty dependencies", function()
local getPolicyImpl = fromMemStorageService({
HttpService = MagicMock.new(),
MemStorageService = MagicMock.new(),
})(behavior)
local connectInstance = connect(getPolicyImpl)
it("SHOULD return a function", function()
expect(connectInstance).to.be.a("function")
end)
describe("GIVEN a static mapper function", function()
local mapper = function()
return {
foo = "bar",
}
end
local mappedConnection = connectInstance(mapper)
it("SHOULD return a function", function()
expect(mappedConnection).to.be.a("function")
end)
describe("GIVEN a component", function()
local baseComponent = function()
return Roact.createElement("Folder")
end
local wrappedComponent = mappedConnection(baseComponent)
it("SHOULD return a new component", function()
expect(baseComponent).to.never.equal(wrappedComponent)
end)
describe("GIVEN a Roact tree without a Provider", function()
local tree = Roact.createElement(wrappedComponent)
it("SHOULD throw", function()
expect(function()
Roact.mount(tree)
end).to.throw()
end)
end)
describe("GIVEN a Roact tree with a Provider", function()
describe("GIVEN a nil policy prop", function()
local tree = Roact.createElement(providerInstance, {
policy = nil,
}, {
wrappedComponent = Roact.createElement(wrappedComponent)
})
it("SHOULD throw", function()
expect(function()
Roact.mount(tree)
end).to.throw()
end)
end)
describe("GIVEN an empty policy prop", function()
local tree = Roact.createElement(providerInstance, {
policy = {},
}, {
wrappedComponent = Roact.createElement(wrappedComponent)
})
it("SHOULD mount and unmount successfully", function()
local instance = Roact.mount(tree)
Roact.unmount(instance)
end)
end)
describe("GIVEN policy prop with a single (static) definition", function()
local mockPolicy1 = function(_)
return {
isFeatureEnabled = "mockPolicy1Enabled",
}
end
it("SHOULD allow mapper to create props for base component", function()
checkForPropsAfterMounting({
connect = connectInstance,
provider = providerInstance,
policyProp = { mockPolicy1 },
mapper = function(policy)
return {
isPolicy1FeatureEnabled = policy.isFeatureEnabled,
}
end,
expectedProps = {
isPolicy1FeatureEnabled = "mockPolicy1Enabled"
},
})
end)
end)
describe("GIVEN policy prop with a multiple (static) definitions", function()
local mockPolicy1 = function(_)
return {
isFeature1Enabled = 100,
}
end
local mockPolicy2 = function(_)
return {
isFeature2Enabled = 200,
}
end
it("SHOULD allow mapper to create props for base component", function()
checkForPropsAfterMounting({
connect = connectInstance,
provider = providerInstance,
policyProp = { mockPolicy1, mockPolicy2 },
mapper = function(policy)
return {
isPolicy1FeatureEnabled = policy.isFeature1Enabled,
isPolicy2FeatureEnabled = policy.isFeature2Enabled,
}
end,
expectedProps = {
isPolicy1FeatureEnabled = 100,
isPolicy2FeatureEnabled = 200,
},
})
end)
end)
end)
end)
end)
end)
describe("GIVEN MemStorageService can retrieve a string and HttpService can decode it", function()
local mockHttpService = MagicMock.new()
mockHttpService.JSONDecode = function(str)
return {
foo = "bar",
}
end
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return "mockStorageData"
end
local getPolicyImpl = fromMemStorageService({
HttpService = mockHttpService,
MemStorageService = mockMemStorageService,
})(behavior)
local connectInstance = connect(getPolicyImpl)
describe("GIVEN policy prop with a single dynamic definition (read from MemStorageService)", function()
local mockPolicy1 = function(policy)
return {
isFeatureEnabled = policy.foo,
}
end
it("SHOULD allow mapper to create props for base component", function()
checkForPropsAfterMounting({
connect = connectInstance,
provider = providerInstance,
policyProp = { mockPolicy1 },
mapper = function(policy)
return {
isPolicy1FeatureEnabled = policy.isFeatureEnabled,
}
end,
expectedProps = {
-- bar is the result from HttpService's JSONDecode
-- (mocking the pull from MemStorageService)
isPolicy1FeatureEnabled = "bar"
},
})
end)
end)
end)
describe("GIVEN MemStorageService cannot retrieve a string", function()
local mockHttpService = MagicMock.new()
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return nil
end
local getPolicyImpl = fromMemStorageService({
HttpService = mockHttpService,
MemStorageService = mockMemStorageService,
})(behavior)
local connectInstance = connect(getPolicyImpl)
describe("GIVEN policy prop with a single dynamic definition (read from MemStorageService)", function()
it("SHOULD allow mapper to create props for base component", function()
local wasMockPolicy1EverCalled = false
local mockPolicy1 = function(policy)
wasMockPolicy1EverCalled = true
-- policy should be an empty table because
-- MemStorageService is providing invalid values
expect(tutils.shallowEqual(policy, {})).to.equal(true)
return {
isFeatureEnabled = policy and policy.foo,
}
end
checkForPropsAfterMounting({
connect = connectInstance,
provider = providerInstance,
policyProp = { mockPolicy1 },
mapper = function(policy)
return {
isPolicy1FeatureEnabled = policy.isFeatureEnabled,
}
end,
expectedProps = {},
})
expect(wasMockPolicy1EverCalled).to.equal(true)
end)
end)
end)
describe("GIVEN MemStorageService updates with a new string", function()
local mockHttpService = MagicMock.new()
mockHttpService.JSONDecode = function(_, jsonString)
if jsonString == "mockJsonString" then
return {
isFeatureEnabled = "hello.world",
}
end
return nil
end
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return nil
end
local updateMemStorageService = Instance.new("BindableEvent")
mockMemStorageService.BindAndFire = function(_, _, func)
return updateMemStorageService.Event:Connect(function(value, a)
func(value)
end)
end
local getPolicyImpl = fromMemStorageService({
HttpService = mockHttpService,
MemStorageService = mockMemStorageService,
})(behavior)
local connectInstance = connect(getPolicyImpl)
describe("GIVEN policy prop with a single dynamic definition (read from MemStorageService)", function()
it("SHOULD allow mapper to create props for base component", function()
local numberOfTimesMockPolicy1EverCalled = 0
local mockPolicy1 = function(policy)
numberOfTimesMockPolicy1EverCalled = numberOfTimesMockPolicy1EverCalled + 1
return {
isFeatureEnabled = policy and policy.isFeatureEnabled,
}
end
checkForPropsAfterMounting({
funcAfterMounting = function()
updateMemStorageService:Fire("mockJsonString")
end,
connect = connectInstance,
provider = providerInstance,
policyProp = { mockPolicy1 },
mapper = function(policy)
return {
isPolicy1FeatureEnabled = policy.isFeatureEnabled,
}
end,
shouldCheckPropsIf = function()
return numberOfTimesMockPolicy1EverCalled > 1
end,
expectedProps = {
isPolicy1FeatureEnabled = "hello.world",
},
})
expect(numberOfTimesMockPolicy1EverCalled).to.equal(2)
end)
end)
end)
describe("GIVEN MemStorageService updates with a nil value", function()
local mockHttpService = MagicMock.new()
mockHttpService.JSONDecode = function(_, jsonString)
if jsonString == "mockJsonString" then
return {
isFeatureEnabled = "hello.world",
}
end
return nil
end
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return nil
end
local updateMemStorageService = Instance.new("BindableEvent")
mockMemStorageService.BindAndFire = function(_, _, func)
return updateMemStorageService.Event:Connect(function(value, a)
func(value)
end)
end
local getPolicyImpl = fromMemStorageService({
HttpService = mockHttpService,
MemStorageService = mockMemStorageService,
})(behavior)
local connectInstance = connect(getPolicyImpl)
describe("GIVEN policy prop with a single dynamic definition (read from MemStorageService)", function()
it("SHOULD allow mapper to create props for base component", function()
local numberOfTimesMockPolicy1EverCalled = 0
local mockPolicy1 = function(policy)
numberOfTimesMockPolicy1EverCalled = numberOfTimesMockPolicy1EverCalled + 1
return {
isFeatureEnabled = policy and policy.isFeatureEnabled,
}
end
checkForPropsAfterMounting({
funcAfterMounting = function()
updateMemStorageService:Fire("mockJsonString")
end,
connect = connectInstance,
provider = providerInstance,
policyProp = { mockPolicy1 },
mapper = function(policy)
return {
isPolicy1FeatureEnabled = policy.isFeatureEnabled,
}
end,
shouldCheckPropsIf = function()
return numberOfTimesMockPolicy1EverCalled > 1
end,
expectedProps = {
isPolicy1FeatureEnabled = "hello.world",
},
})
expect(numberOfTimesMockPolicy1EverCalled).to.equal(2)
end)
end)
end)
describe("GIVEN a policyData prop", function()
local mockHttpService = MagicMock.new()
local mockMemStorageService = MagicMock.new()
local getPolicyImpl = fromMemStorageService({
HttpService = mockHttpService,
MemStorageService = mockMemStorageService,
})(behavior)
local connectInstance = connect(getPolicyImpl)
local mockPolicy1 = function(policy)
return {
isFeatureEnabled = policy.mockIsFeatureEnabled,
}
end
it("SHOULD return policyData and not call MemStorageService", function()
checkForPropsAfterMounting({
connect = connectInstance,
provider = providerInstance,
policyProp = { mockPolicy1 },
policyDataProp = {
mockIsFeatureEnabled = "hello.world",
},
mapper = function(policy)
return {
isPolicy1FeatureEnabled = policy.isFeatureEnabled,
}
end,
expectedProps = {
isPolicy1FeatureEnabled = "hello.world",
},
})
local callsGetItem = Mock.getCalls(mockMemStorageService.GetItem)
expect(#callsGetItem).to.equal(0)
local callsBindAndFire = Mock.getCalls(mockMemStorageService.BindAndFire)
expect(#callsBindAndFire).to.equal(0)
end)
end)
end)
describe("GIVEN a policy prop that reads `foo`", function()
local mockPolicy1 = function(policy)
return {
isFeatureEnabled = policy.foo,
}
end
describe("GIVEN getPolicyImpl with a static response", function()
local getPolicyImpl = MagicMock.new()
getPolicyImpl.read = function()
return {
foo = "bar",
}
end
local connectInstance = connect(getPolicyImpl)
it("SHOULD pass props to the lower component from the Promise resolution", function()
checkForPropsAfterMounting({
connect = connectInstance,
provider = providerInstance,
policyProp = { mockPolicy1 },
mapper = function(policy)
return {
isPolicy1FeatureEnabled = policy.isFeatureEnabled,
hasProblemsFetching = (policy.isFeatureEnabled == nil),
}
end,
expectedProps = {
isPolicy1FeatureEnabled = "bar",
hasProblemsFetching = false,
},
})
end)
end)
describe("GIVEN getPolicyImpl with a nil response", function()
local getPolicyImpl = MagicMock.new()
getPolicyImpl.read = function()
return nil
end
local connectInstance = connect(getPolicyImpl)
it("SHOULD not be able to define `isPolicy1FeatureEnabled`", function()
checkForPropsAfterMounting({
connect = connectInstance,
provider = providerInstance,
policyProp = { mockPolicy1 },
mapper = function(policy)
return {
isPolicy1FeatureEnabled = policy.isFeatureEnabled,
hasProblemsFetching = (policy.isFeatureEnabled == nil)
}
end,
expectedProps = {
isPolicy1FeatureEnabled = nil,
hasProblemsFetching = true,
},
})
end)
end)
end)
end)
end
@@ -0,0 +1,64 @@
return function()
local Packages = script.Parent.Parent
local Roact = require(Packages.Roact)
local PolicyProvider = require(script.Parent)
it("SHOULD work", function()
local presentationPolicy = function(externalPolicy)
return {
isFeatureEnabled = externalPolicy.isFeatureEnabled or false,
}
end
local externalPolicy = {
isFeatureEnabled = true,
}
local getPolicyImpl = PolicyProvider.GetPolicyImplementations.Static(externalPolicy)
local UniversalAppPolicyProvider = PolicyProvider.withGetPolicyImplementation(getPolicyImpl)
local function MyComponent(props)
-- Values from PolicyProvider can be accessed just like regular props
local isFeatureEnabled = props.isFeatureEnabled
return Roact.createElement("ScreenGui", nil, {
Label = Roact.createElement("TextLabel", {
-- ...and used in your components!
Text = "isFeatureEnabled: " .. tostring(isFeatureEnabled),
Size = UDim2.new(1, 0, 1, 0),
})
})
end
-- `mapPolicyToProps` should return a table containing props that will be passed to
-- your component!
-- `connect` returns a function, so we call that function, passing in our
-- component, getting back a new component!
MyComponent = UniversalAppPolicyProvider.connect(
function(incomingPresentationPolicy, props)
-- mapPolicyToProps is run every time the policy's state updates.
-- It's also run whenever the component receives new props.
return {
isFeatureEnabled = incomingPresentationPolicy.isFeatureEnabled,
}
end
)(MyComponent)
local app = Roact.createElement(UniversalAppPolicyProvider.Provider, {
-- policy can be a list of PresentationalPolicies
policy = { presentationPolicy },
}, {
Main = Roact.createElement(MyComponent),
})
local folder = Instance.new("Folder")
Roact.mount(app, folder)
-- if the label has the following string, we know
-- everything is working!
local Label = folder:FindFirstChild("Label", true)
expect(Label).to.be.ok()
expect(Label.Text).to.equal("isFeatureEnabled: true")
end)
end
@@ -0,0 +1,98 @@
local DefaultHttpService = game:GetService("HttpService")
local DefaultMemStorageService = game:GetService("MemStorageService")
local DefaultPlayersService = game:GetService("Players")
return function(dependencies)
dependencies = dependencies or {}
dependencies.HttpService = dependencies.HttpService or DefaultHttpService
dependencies.MemStorageService = dependencies.MemStorageService or DefaultMemStorageService
dependencies.PlayersService = dependencies.PlayersService or DefaultPlayersService
assert(dependencies.HttpService, "expected dependencies.HttpService")
assert(dependencies.MemStorageService, "expected dependencies.MemStorageService")
assert(dependencies.PlayersService, "expected dependencies.PlayersService")
local HttpService = dependencies.HttpService
local MemStorageService = dependencies.MemStorageService
local PlayersService = dependencies.PlayersService
return function(behavior)
assert(behavior, "expected behavior")
local function getStoreKey()
local userId = -1
if PlayersService.LocalPlayer then
userId = PlayersService.LocalPlayer.UserId
end
return "GUAC:" .. userId .. ":" .. behavior
end
local connectionStoreKey
local memStorageConnection
local previouslyReadValue
local onPolicyChangedEvent = Instance.new("BindableEvent")
local function onPolicyUpdated(newPolicyData)
-- MemStorageService will not de-duplicate the same item from storage
if newPolicyData ~= previouslyReadValue then
if newPolicyData and #newPolicyData > 0 then
local success, decodedExternalPolicy = pcall(function()
return HttpService:JSONDecode(newPolicyData)
end)
if success then
-- never store garbage
previouslyReadValue = newPolicyData
onPolicyChangedEvent:Fire(decodedExternalPolicy)
end
end
end
end
return {
read = function()
local storeKey = getStoreKey()
local policyData = MemStorageService:GetItem(storeKey)
if policyData and #policyData > 0 then
local success, policy = pcall(function()
return HttpService:JSONDecode(policyData)
end)
if success then
-- Be sure to store the json string
previouslyReadValue = policyData
return policy
end
end
return nil
end,
onPolicyChanged = function(func)
local storeKey = getStoreKey()
local connection = onPolicyChangedEvent.Event:Connect(func)
if memStorageConnection and connectionStoreKey == storeKey then
-- Fire listener with existing value
if previouslyReadValue then
local success, policy = pcall(function()
return HttpService:JSONDecode(previouslyReadValue)
end)
if success then
func(policy)
end
end
else
if memStorageConnection then
memStorageConnection:Disconnect()
end
connectionStoreKey = storeKey
memStorageConnection = MemStorageService:BindAndFire(storeKey, onPolicyUpdated)
end
return connection
end,
}
end
end
@@ -0,0 +1,304 @@
return function()
local Packages = script.Parent.Parent.Parent
local Mock = require(Packages.Mock)
local MagicMock = Mock.MagicMock
local fromMemStorageService = require(script.Parent.fromMemStorageService)
describe("GIVEN a behavior", function()
local behavior = "mockBehavior"
describe("GIVEN a MemStorageService and HttpService with functional GetItem, BindAndFire and JSONDecode", function()
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return "jsonExternalPolicy"
end
local updateMemStorageService = Instance.new("BindableEvent")
mockMemStorageService.BindAndFire = function(_, _, func)
local initialValue = mockMemStorageService.GetItem()
local connection = updateMemStorageService.Event:Connect(function(value, a)
func(value)
end)
func(initialValue)
return connection
end
local mockHttpService = MagicMock.new()
mockHttpService.JSONDecode = function()
return "decodedExternalPolicy"
end
local fromMemStorageServiceInstance = fromMemStorageService({
MemStorageService = mockMemStorageService,
HttpService = mockHttpService,
})(behavior)
it("SHOULD return the policy when `read` is invoked", function()
local result = fromMemStorageServiceInstance.read()
expect(result).to.equal("decodedExternalPolicy")
end)
it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function()
local result = fromMemStorageServiceInstance.onPolicyChanged(function() end)
expect(result).to.be.ok()
expect(result.Disconnect).to.be.ok()
result:Disconnect()
end)
it("SHOULD invoke passed in function with JSONDecode results when updateMemStorageService is fired", function()
local wasEverCalled = false
local result = fromMemStorageServiceInstance.onPolicyChanged(function(value)
wasEverCalled = true
expect(value).to.equal("decodedExternalPolicy")
end)
updateMemStorageService:Fire("jsonExternalPolicyUpdated")
result:Disconnect()
expect(wasEverCalled).to.equal(true)
end)
it("SHOULD NOT invoke passed in function with JSONDecode results "..
"when updateMemStorageService is fired with the same value", function()
local timesEverCalled = 0
local result = fromMemStorageServiceInstance.onPolicyChanged(function(value)
timesEverCalled = timesEverCalled + 1
expect(value).to.equal("decodedExternalPolicy")
end)
updateMemStorageService:Fire("foo")
updateMemStorageService:Fire("foo")
updateMemStorageService:Fire("foo")
updateMemStorageService:Fire("foo")
result:Disconnect()
-- Once for initial value and then once for foo
expect(timesEverCalled).to.equal(2)
end)
it("SHOULD NOT invoke passed in function with JSONDecode results "..
"when updateMemStorageService is fired with the same value, while ignore nils", function()
local timesEverCalled = 0
local result = fromMemStorageServiceInstance.onPolicyChanged(function(value)
timesEverCalled = timesEverCalled + 1
expect(value).to.equal("decodedExternalPolicy")
end)
updateMemStorageService:Fire("bar")
updateMemStorageService:Fire(nil)
updateMemStorageService:Fire("bar")
result:Disconnect()
-- Once for initial value and then once for bar
expect(timesEverCalled).to.equal(2)
end)
it("SHOULD invoke multiple listeners when updateMemStorageService is fired", function()
local listenerACalled = 0
local connA = fromMemStorageServiceInstance.onPolicyChanged(function(value)
listenerACalled = listenerACalled + 1
expect(value).to.equal("decodedExternalPolicy")
end)
local listenerBCalled = 0
local connB = fromMemStorageServiceInstance.onPolicyChanged(function(value)
listenerBCalled = listenerBCalled + 1
expect(value).to.equal("decodedExternalPolicy")
end)
updateMemStorageService:Fire("baz")
connA:Disconnect()
connB:Disconnect()
-- Once for initial value and then once for baz
expect(listenerACalled).to.equal(2)
expect(listenerBCalled).to.equal(2)
end)
end)
describe("GIVEN a functional MemStorageService and broken HttpService JSONDecode", function()
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return "jsonExternalPolicy"
end
local updateMemStorageService = Instance.new("BindableEvent")
mockMemStorageService.BindAndFire = function(_, _, func)
return updateMemStorageService.Event:Connect(function(value, a)
func(value)
end)
end
local mockHttpService = MagicMock.new()
mockHttpService.JSONDecode = function()
return nil
end
local fromMemStorageServiceInstance = fromMemStorageService({
MemStorageService = mockMemStorageService,
HttpService = mockHttpService,
})(behavior)
it("SHOULD return nil when `read` is invoked", function()
local result = fromMemStorageServiceInstance.read()
expect(result).to.equal(nil)
end)
it("SHOULD invoke passed in function with JSONDecode results when updateMemStorageService is fired", function()
local wasEverCalled = false
local result = fromMemStorageServiceInstance.onPolicyChanged(function(value)
wasEverCalled = true
expect(value).to.equal(nil)
end)
updateMemStorageService:Fire("jsonExternalPolicyUpdated")
result:Disconnect()
expect(wasEverCalled).to.equal(true)
end)
end)
describe("GIVEN a MemStorageService that always returns invalid results", function()
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return nil
end
local updateMemStorageService = Instance.new("BindableEvent")
mockMemStorageService.BindAndFire = function(_, _, func)
return updateMemStorageService.Event:Connect(function(value, a)
func(value)
end)
end
local mockHttpService = MagicMock.new()
local fromMemStorageServiceInstance = fromMemStorageService({
MemStorageService = mockMemStorageService,
HttpService = mockHttpService,
})(behavior)
it("SHOULD return nil when `read` is invoked", function()
local result = fromMemStorageServiceInstance.read()
expect(result).to.equal(nil)
end)
it("SHOULD never invoke passed function with JSONDecode results when updateMemStorageService is fired", function()
local wasEverCalled = false
local result = fromMemStorageServiceInstance.onPolicyChanged(function(value)
wasEverCalled = true
end)
updateMemStorageService:Fire(nil)
result:Disconnect()
expect(wasEverCalled).to.equal(false)
end)
end)
describe("GIVEN a PlayersService with a missing LocalPlayer", function()
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return "mockStorageItemJSON"
end
local mockHttpService = MagicMock.new()
local mockPlayersService = MagicMock.new()
mockPlayersService.LocalPlayer = nil
local fromMemStorageServiceInstance = fromMemStorageService({
MemStorageService = mockMemStorageService,
PlayersService = mockPlayersService,
HttpService = mockHttpService,
})(behavior)
it("SHOULD still return a value when `read` is invoked", function()
local result = fromMemStorageServiceInstance.read()
expect(result).to.be.ok()
end)
end)
describe("GIVEN a HttpService that can throw based on MemStorageService's GetItem", function()
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return "garbage"
end
local updateMemStorageService = Instance.new("BindableEvent")
mockMemStorageService.BindAndFire = function(_, _, func)
return updateMemStorageService.Event:Connect(function(value, a)
func(value)
end)
end
local mockHttpService = MagicMock.new()
mockHttpService.JSONDecode = function(_, value)
if value == "validJson" then
return { foo = true }
end
error("invalid json")
end
local fromMemStorageServiceInstance = fromMemStorageService({
MemStorageService = mockMemStorageService,
HttpService = mockHttpService,
})(behavior)
it("SHOULD return nil since GetItem returns garbage", function()
local result = fromMemStorageServiceInstance.read()
expect(result).to.never.be.ok()
end)
it("SHOULD never invoke passed function with JSONDecode results when updateMemStorageService is fired", function()
local numberOfTimesCalled = 0
local result = fromMemStorageServiceInstance.onPolicyChanged(function(value)
numberOfTimesCalled = numberOfTimesCalled + 1
end)
updateMemStorageService:Fire("validJson")
updateMemStorageService:Fire("garbage")
updateMemStorageService:Fire("validJson")
result:Disconnect()
expect(numberOfTimesCalled).to.equal(1)
end)
end)
describe("GIVEN a new instance of fromMemStorageService", function()
local validJson = "validJson"
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return validJson
end
local updateMemStorageService = Instance.new("BindableEvent")
mockMemStorageService.BindAndFire = function(_, _, func)
return updateMemStorageService.Event:Connect(function(value, a)
func(value)
end)
end
local mockHttpService = MagicMock.new()
mockHttpService.JSONDecode = function(_, value)
return "tableFromJson"
end
local fromMemStorageServiceInstance = fromMemStorageService({
MemStorageService = mockMemStorageService,
HttpService = mockHttpService,
})(behavior)
describe("WHEN read returns a value and onPolicyChanged returns the same value", function()
it("SHOULD not fire onPolicyChanged function", function()
local numberOfTimesCalled = 0
fromMemStorageServiceInstance.onPolicyChanged(function()
numberOfTimesCalled = numberOfTimesCalled + 1
end)
local _ = fromMemStorageServiceInstance.read()
-- intentionally fire the same value that we read
updateMemStorageService:Fire(validJson)
expect(numberOfTimesCalled).to.equal(0)
end)
end)
end)
end)
end
@@ -0,0 +1,61 @@
local DefaultPolicyService = game:GetService("PolicyService")
local DefaultPlayersService = game:GetService("Players")
local root = script.Parent.Parent
local Packages = root.Parent
local Promise = require(Packages.Promise)
local Logger = require(root.Logger)
return function(dependencies)
dependencies = dependencies or {}
dependencies.PolicyService = dependencies.PolicyService or DefaultPolicyService
dependencies.PlayersService = dependencies.PlayersService or DefaultPlayersService
assert(dependencies.PolicyService, "expected dependencies.PolicyService")
assert(dependencies.PlayersService, "expected dependencies.PlayersService")
local PolicyService = dependencies.PolicyService
local PlayersService = dependencies.PlayersService
return function()
return {
read = function()
return nil
end,
onPolicyChanged = function(func)
local onPolicyChangedEvent = Instance.new("BindableEvent")
-- be sure to connect before our Promise can resolve
local connection = onPolicyChangedEvent.Event:Connect(func)
Promise.new(function(resolve, reject)
local player = PlayersService.LocalPlayer
if player then
local success, result = pcall(function()
return PolicyService:GetPolicyInfoForPlayerAsync(player)
end)
if success then
if result then
resolve(result)
else
reject("GetPolicyInfoForPlayerAsync return nil value")
end
else
reject("GetPolicyInfoForPlayerAsync had an error when calling")
end
else
reject("LocalPlayer not found")
end
end):andThen(function(newPolicy)
onPolicyChangedEvent:Fire(newPolicy)
end):catch(function(errorString)
Logger:warning("Could not fetch from PolicyService due to error: {}", errorString)
end)
return connection
end,
}
end
end
@@ -0,0 +1,160 @@
return function()
local Packages = script.Parent.Parent.Parent
local Mock = require(Packages.Mock)
local MagicMock = Mock.MagicMock
local fromPolicyService = require(script.Parent.fromPolicyService)
describe("GIVEN a fully mocked dependencies", function()
local mockPolicyService = MagicMock.new()
mockPolicyService.GetPolicyInfoForPlayerAsync = function()
return "mockGetPolicyInfoForPlayerAsyncResponse"
end
local dependencies = {
PolicyService = mockPolicyService,
PlayersService = MagicMock.new(),
}
describe("WHEN invoked", function()
local fromPolicyServiceInstance = fromPolicyService(dependencies)()
it("SHOULD return a table", function()
expect(fromPolicyServiceInstance).to.be.a("table")
end)
it("SHOULD return a table with read and `onPolicyChanged` fields", function()
expect(fromPolicyServiceInstance.read).to.be.a("function")
expect(fromPolicyServiceInstance.onPolicyChanged).to.be.a("function")
end)
describe("WHEN `onPolicyChanged` is invoked", function()
local timesEverCalled = 0
local lastValue
local connection = fromPolicyServiceInstance.onPolicyChanged(function(value)
timesEverCalled = timesEverCalled + 1
lastValue = value
end)
it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function()
expect(connection).to.be.ok()
expect(connection.Disconnect).to.be.ok()
end)
it("SHOULD fire when it resolves with data", function()
expect(timesEverCalled).to.equal(1)
expect(lastValue).to.equal("mockGetPolicyInfoForPlayerAsyncResponse")
end)
end)
describe("WHEN `read` is invoked", function()
local result = fromPolicyServiceInstance.read()
it("SHOULD return nil", function()
expect(result).to.never.be.ok()
end)
end)
end)
end)
describe("GIVEN a PlayersService with no LocalPlayer", function()
local mockPlayersService = MagicMock.new()
mockPlayersService.LocalPlayer = nil
local dependencies = {
PolicyService = MagicMock.new(),
PlayersService = mockPlayersService,
}
describe("WHEN invoked", function()
local fromPolicyServiceInstance = fromPolicyService(dependencies)()
it("SHOULD return a table", function()
expect(fromPolicyServiceInstance).to.be.a("table")
end)
describe("WHEN `onPolicyChanged` is invoked", function()
local timesEverCalled = 0
local connection = fromPolicyServiceInstance.onPolicyChanged(function(value)
timesEverCalled = timesEverCalled + 1
end)
it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function()
expect(connection).to.be.ok()
expect(connection.Disconnect).to.be.ok()
end)
it("SHOULD never fire when it rejects", function()
expect(timesEverCalled).to.equal(0)
end)
end)
end)
end)
describe("GIVEN a PolicyService that throws", function()
local mockPolicyService = MagicMock.new()
mockPolicyService.GetPolicyInfoForPlayerAsync = function()
error("Throw")
end
local dependencies = {
PolicyService = mockPolicyService,
PlayersService = MagicMock.new(),
}
describe("WHEN invoked", function()
local fromPolicyServiceInstance = fromPolicyService(dependencies)()
it("SHOULD return a table", function()
expect(fromPolicyServiceInstance).to.be.a("table")
end)
describe("WHEN `onPolicyChanged` is invoked", function()
local timesEverCalled = 0
local connection = fromPolicyServiceInstance.onPolicyChanged(function(value)
timesEverCalled = timesEverCalled + 1
end)
it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function()
expect(connection).to.be.ok()
expect(connection.Disconnect).to.be.ok()
end)
it("SHOULD never fire when it rejects", function()
expect(timesEverCalled).to.equal(0)
end)
end)
end)
end)
describe("GIVEN a PolicyService that returns nil", function()
local mockPolicyService = MagicMock.new()
mockPolicyService.GetPolicyInfoForPlayerAsync = function()
return nil
end
local dependencies = {
PolicyService = mockPolicyService,
PlayersService = MagicMock.new(),
}
describe("WHEN invoked", function()
local fromPolicyServiceInstance = fromPolicyService(dependencies)()
it("SHOULD return a table", function()
expect(fromPolicyServiceInstance).to.be.a("table")
end)
describe("WHEN `onPolicyChanged` is invoked", function()
local timesEverCalled = 0
local connection = fromPolicyServiceInstance.onPolicyChanged(function(value)
timesEverCalled = timesEverCalled + 1
end)
it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function()
expect(connection).to.be.ok()
expect(connection.Disconnect).to.be.ok()
end)
it("SHOULD never fire when it rejects", function()
expect(timesEverCalled).to.equal(0)
end)
end)
end)
end)
end
@@ -0,0 +1,19 @@
local function noOpt()
end
return function()
return function(externalPolicy)
assert(externalPolicy, "expected externalPolicy")
return {
read = function()
return externalPolicy
end,
onPolicyChanged = function(func)
func = func or noOpt
return Instance.new("BindableEvent").Event:Connect(func)
end,
}
end
end
@@ -0,0 +1,45 @@
return function()
describe("WHEN required", function()
local fromStaticSource = require(script.Parent.fromStaticSource)
it("SHOULD have the following interface", function()
expect(fromStaticSource).to.be.a("function")
end)
describe("WHEN invoked", function()
local fromStaticSourceInstance = fromStaticSource()
it("SHOULD have the following interface", function()
expect(fromStaticSourceInstance).to.be.a("function")
end)
describe("GIVEN a static value", function()
local mockExternalPolicy = "mockExternalPolicy"
describe("WHEN invoked", function()
local fromStaticSourceInstanceWithExternalPolicy = fromStaticSourceInstance(mockExternalPolicy)
it("SHOULD have the following interface", function()
expect(fromStaticSourceInstanceWithExternalPolicy).to.be.a("table")
expect(fromStaticSourceInstanceWithExternalPolicy.read).to.be.a("function")
expect(fromStaticSourceInstanceWithExternalPolicy.onPolicyChanged).to.be.a("function")
end)
describe("WHEN read is invoked", function()
local result = fromStaticSourceInstanceWithExternalPolicy.read()
it("SHOULD return the static value", function()
expect(result).to.equal(mockExternalPolicy)
end)
end)
describe("WHEN onPolicyChanged is invoked", function()
local result = fromStaticSourceInstanceWithExternalPolicy.onPolicyChanged()
it("SHOULD return a Disconnect-able object", function()
expect(result).to.be.ok()
expect(result.Disconnect).to.be.ok()
end)
end)
end)
end)
end)
end)
end
@@ -0,0 +1,74 @@
local DefaultHttpService = game:GetService("HttpService")
local DefaultMemStorageService = game:GetService("MemStorageService")
local DefaultPlayersService = game:GetService("Players")
return function(dependencies)
dependencies = dependencies or {}
dependencies.HttpService = dependencies.HttpService or DefaultHttpService
dependencies.MemStorageService = dependencies.MemStorageService or DefaultMemStorageService
dependencies.PlayersService = dependencies.PlayersService or DefaultPlayersService
assert(dependencies.HttpService, "expected dependencies.HttpService")
assert(dependencies.MemStorageService, "expected dependencies.MemStorageService")
assert(dependencies.PlayersService, "expected dependencies.PlayersService")
local HttpService = dependencies.HttpService
local MemStorageService = dependencies.MemStorageService
local PlayersService = dependencies.PlayersService
return function(behavior)
assert(behavior, "expected behavior")
local function getStoreKey()
local userId = -1
if PlayersService.LocalPlayer then
userId = PlayersService.LocalPlayer.UserId
end
return "GUAC:" .. userId .. ":" .. behavior
end
local previouslyReadValue
return {
read = function()
local storeKey = getStoreKey()
local policyData = MemStorageService:GetItem(storeKey)
if policyData and #policyData > 0 then
local success, policy = pcall(function()
return HttpService:JSONDecode(policyData)
end)
if success then
-- Be sure to store the json string
previouslyReadValue = policyData
return policy
end
end
return nil
end,
onPolicyChanged = function(func)
local storeKey = getStoreKey()
local memStorageConnection = MemStorageService:BindAndFire(storeKey, function(newPolicyData)
-- MemStorageService will not du-duplicate the same item from storage
if newPolicyData ~= previouslyReadValue then
if newPolicyData and #newPolicyData > 0 then
local success, decodedExternalPolicy = pcall(function()
return HttpService:JSONDecode(newPolicyData)
end)
if success then
-- never store garbage
previouslyReadValue = newPolicyData
if func then
func(decodedExternalPolicy)
end
end
end
end
end)
return memStorageConnection
end,
}
end
end
@@ -0,0 +1,273 @@
return function()
local Packages = script.Parent.Parent.Parent
local Mock = require(Packages.Mock)
local MagicMock = Mock.MagicMock
local fromMemStorageService = require(script.Parent.oldFromMemStorageService)
describe("GIVEN a behavior", function()
local behavior = "mockBehavior"
describe("GIVEN a MemStorageService and HttpService with functional GetItem, BindAndFire and JSONDecode", function()
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return "jsonExternalPolicy"
end
local updateMemStorageService = Instance.new("BindableEvent")
mockMemStorageService.BindAndFire = function(_, _, func)
return updateMemStorageService.Event:Connect(function(value, a)
func(value)
end)
end
local mockHttpService = MagicMock.new()
mockHttpService.JSONDecode = function()
return "decodedExternalPolicy"
end
local fromMemStorageServiceInstance = fromMemStorageService({
MemStorageService = mockMemStorageService,
HttpService = mockHttpService,
})(behavior)
it("SHOULD return the policy when `read` is invoked", function()
local result = fromMemStorageServiceInstance.read()
expect(result).to.equal("decodedExternalPolicy")
end)
it("SHOULD return a Disconnect-able object when `onPolicyChanged` is invoked", function()
local result = fromMemStorageServiceInstance.onPolicyChanged()
expect(result).to.be.ok()
expect(result.Disconnect).to.be.ok()
result:Disconnect()
end)
it("SHOULD invoke passed in function with JSONDecode results when updateMemStorageService is fired", function()
local wasEverCalled = false
local result = fromMemStorageServiceInstance.onPolicyChanged(function(value)
wasEverCalled = true
expect(value).to.equal("decodedExternalPolicy")
end)
updateMemStorageService:Fire("jsonExternalPolicyUpdated")
result:Disconnect()
expect(wasEverCalled).to.equal(true)
end)
it("SHOULD NOT invoke passed in function with JSONDecode results "..
"when updateMemStorageService is fired with the same value", function()
local timesEverCalled = 0
local result = fromMemStorageServiceInstance.onPolicyChanged(function(value)
timesEverCalled = timesEverCalled + 1
expect(value).to.equal("decodedExternalPolicy")
end)
updateMemStorageService:Fire("foo")
updateMemStorageService:Fire("foo")
result:Disconnect()
expect(timesEverCalled).to.equal(1)
end)
it("SHOULD NOT invoke passed in function with JSONDecode results "..
"when updateMemStorageService is fired with the same value, while ignore nils", function()
local timesEverCalled = 0
local result = fromMemStorageServiceInstance.onPolicyChanged(function(value)
timesEverCalled = timesEverCalled + 1
expect(value).to.equal("decodedExternalPolicy")
end)
updateMemStorageService:Fire("bar")
updateMemStorageService:Fire(nil)
updateMemStorageService:Fire("bar")
result:Disconnect()
expect(timesEverCalled).to.equal(1)
end)
end)
describe("GIVEN a functional MemStorageService and broken HttpService JSONDecode", function()
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return "jsonExternalPolicy"
end
local updateMemStorageService = Instance.new("BindableEvent")
mockMemStorageService.BindAndFire = function(_, _, func)
return updateMemStorageService.Event:Connect(function(value, a)
func(value)
end)
end
local mockHttpService = MagicMock.new()
mockHttpService.JSONDecode = function()
return nil
end
local fromMemStorageServiceInstance = fromMemStorageService({
MemStorageService = mockMemStorageService,
HttpService = mockHttpService,
})(behavior)
it("SHOULD return nil when `read` is invoked", function()
local result = fromMemStorageServiceInstance.read()
expect(result).to.equal(nil)
end)
it("SHOULD invoke passed in function with JSONDecode results when updateMemStorageService is fired", function()
local wasEverCalled = false
local result = fromMemStorageServiceInstance.onPolicyChanged(function(value)
wasEverCalled = true
expect(value).to.equal(nil)
end)
updateMemStorageService:Fire("jsonExternalPolicyUpdated")
result:Disconnect()
expect(wasEverCalled).to.equal(true)
end)
end)
describe("GIVEN a MemStorageService that always returns invalid results", function()
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return nil
end
local updateMemStorageService = Instance.new("BindableEvent")
mockMemStorageService.BindAndFire = function(_, _, func)
return updateMemStorageService.Event:Connect(function(value, a)
func(value)
end)
end
local mockHttpService = MagicMock.new()
local fromMemStorageServiceInstance = fromMemStorageService({
MemStorageService = mockMemStorageService,
HttpService = mockHttpService,
})(behavior)
it("SHOULD return nil when `read` is invoked", function()
local result = fromMemStorageServiceInstance.read()
expect(result).to.equal(nil)
end)
it("SHOULD never invoke passed function with JSONDecode results when updateMemStorageService is fired", function()
local wasEverCalled = false
local result = fromMemStorageServiceInstance.onPolicyChanged(function(value)
wasEverCalled = true
end)
updateMemStorageService:Fire(nil)
result:Disconnect()
expect(wasEverCalled).to.equal(false)
end)
end)
describe("GIVEN a PlayersService with a missing LocalPlayer", function()
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return "mockStorageItemJSON"
end
local mockHttpService = MagicMock.new()
local mockPlayersService = MagicMock.new()
mockPlayersService.LocalPlayer = nil
local fromMemStorageServiceInstance = fromMemStorageService({
MemStorageService = mockMemStorageService,
PlayersService = mockPlayersService,
HttpService = mockHttpService,
})(behavior)
it("SHOULD still return a value when `read` is invoked", function()
local result = fromMemStorageServiceInstance.read()
expect(result).to.be.ok()
end)
end)
describe("GIVEN a HttpService that can throw based on MemStorageService's GetItem", function()
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return "garbage"
end
local updateMemStorageService = Instance.new("BindableEvent")
mockMemStorageService.BindAndFire = function(_, _, func)
return updateMemStorageService.Event:Connect(function(value, a)
func(value)
end)
end
local mockHttpService = MagicMock.new()
mockHttpService.JSONDecode = function(_, value)
if value == "validJson" then
return { foo = true }
end
error("invalid json")
end
local fromMemStorageServiceInstance = fromMemStorageService({
MemStorageService = mockMemStorageService,
HttpService = mockHttpService,
})(behavior)
it("SHOULD return nil since GetItem returns garbage", function()
local result = fromMemStorageServiceInstance.read()
expect(result).to.never.be.ok()
end)
it("SHOULD never invoke passed function with JSONDecode results when updateMemStorageService is fired", function()
local numberOfTimesCalled = 0
local result = fromMemStorageServiceInstance.onPolicyChanged(function(value)
numberOfTimesCalled = numberOfTimesCalled + 1
end)
updateMemStorageService:Fire("validJson")
updateMemStorageService:Fire("garbage")
updateMemStorageService:Fire("validJson")
result:Disconnect()
expect(numberOfTimesCalled).to.equal(1)
end)
end)
describe("GIVEN a new instance of fromMemStorageService", function()
local validJson = "validJson"
local mockMemStorageService = MagicMock.new()
mockMemStorageService.GetItem = function()
return validJson
end
local updateMemStorageService = Instance.new("BindableEvent")
mockMemStorageService.BindAndFire = function(_, _, func)
return updateMemStorageService.Event:Connect(function(value, a)
func(value)
end)
end
local mockHttpService = MagicMock.new()
mockHttpService.JSONDecode = function(_, value)
return "tableFromJson"
end
local fromMemStorageServiceInstance = fromMemStorageService({
MemStorageService = mockMemStorageService,
HttpService = mockHttpService,
})(behavior)
describe("WHEN read returns a value and onPolicyChanged returns the same value", function()
it("SHOULD not fire onPolicyChanged function", function()
local numberOfTimesCalled = 0
fromMemStorageServiceInstance.onPolicyChanged(function()
numberOfTimesCalled = numberOfTimesCalled + 1
end)
local result = fromMemStorageServiceInstance.read()
-- intentionally fire the same value that we read
updateMemStorageService:Fire(validJson)
expect(numberOfTimesCalled).to.equal(0)
end)
end)
end)
end)
end
@@ -0,0 +1,36 @@
local connect = require(script.connect)
local Provider = require(script.Provider)
local Logger = require(script.Logger)
local GetPolicyImplementations = script.getPolicyImplementations
local fromMemStorageService
local fromPolicyService = require(GetPolicyImplementations.fromPolicyService)
local fromStaticSource = require(GetPolicyImplementations.fromStaticSource)
local FFlagPolicyProviderFromMemStorageServiceFix = game:DefineFastFlag("PolicyProviderFromMemStorageServiceFix", false)
if FFlagPolicyProviderFromMemStorageServiceFix then
fromMemStorageService = require(GetPolicyImplementations.fromMemStorageService)
else
fromMemStorageService = require(GetPolicyImplementations.oldFromMemStorageService)
end
return {
withGetPolicyImplementation = function(getPolicyImpl)
-- assign default
assert(getPolicyImpl.read, "expected getPolicyImpl to have `read` function")
assert(getPolicyImpl.onPolicyChanged, "expected getPolicyImpl to have `onPolicyChanged` function")
return {
connect = connect(getPolicyImpl),
Provider = Provider(),
}
end,
GetPolicyImplementations = {
MemStorageService = fromMemStorageService(),
PolicyService = fromPolicyService(),
Static = fromStaticSource(),
},
Logger = Logger,
}
@@ -0,0 +1,82 @@
return function()
local Packages = script.Parent.Parent
local tutils = require(Packages.tutils)
local Cryo = require(Packages.Cryo)
-- Given an interface and a list of expected interface members,
-- this expectation will fail the test
local function expectInterface(actualInterface, expectedInterface)
-- build a list of keys that are in the actualInterface
local foundKeys = Cryo.List.map(expectedInterface, function(value)
if actualInterface[value] then
return value
end
end)
-- compare the list of keys found in actualInterface and expectedInterface
local result = tutils.shallowEqual(foundKeys, expectedInterface)
if not result then
local differences = tutils.listDifferences(expectedInterface, foundKeys)
fail(string.format("Expected interface missing the following: %s", tutils.toString(differences)))
end
end
describe("WHEN require is invoked", function()
local PolicyProvider = require(script.Parent)
it("SHOULD have the following public interface", function()
expectInterface(PolicyProvider, { "withGetPolicyImplementation", "GetPolicyImplementations", "Logger" })
end)
it("SHOULD have all GetPolicyImplementations", function()
expectInterface(PolicyProvider.GetPolicyImplementations, { "MemStorageService", "PolicyService", "Static" })
end)
describe("WHEN withGetPolicyImplementation is invoked", function()
describe("GIVEN a stubbed GetPolicyImpl", function()
local stubbedGetPolicyImpl = {
read = function()
end,
onPolicyChanged = function()
end,
}
it("SHOULD have the following public interface", function()
local initializedProvider = PolicyProvider.withGetPolicyImplementation(stubbedGetPolicyImpl)
expectInterface(initializedProvider, { "connect", "Provider" })
end)
end)
describe("GIVEN a nothing", function()
it("SHOULD throw", function()
expect(function()
PolicyProvider.withGetPolicyImplementation()
end).to.throw()
end)
end)
describe("GIVEN a GetPolicyImpl without a read", function()
local withoutRead = {
onPolicyChanged = function()
end
}
it("SHOULD throw", function()
expect(function()
PolicyProvider.withGetPolicyImplementation(withoutRead)
end).to.throw()
end)
end)
describe("GIVEN a GetPolicyImpl without a onPolicyChanged", function()
local withoutOnPolicyChanged = {
read = function()
end
}
it("SHOULD throw", function()
expect(function()
PolicyProvider.withGetPolicyImplementation(withoutOnPolicyChanged)
end).to.throw()
end)
end)
end)
end)
end
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["tutils"]["tutils"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package