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"]["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 = "1d595dae48c654c54a76f07a2ddb95bfad53dc43"
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 47552268 git+https://github.rbx.com/roblox/lumberyak#master",
"Mock jtaylor/mock d2c4005c git+https://github.rbx.com/roblox/mock#master",
"Promise lua-promise bbb9e162 git+https://github.rbx.com/roblox/lua-promise#master",
"Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact",
"Symbol roblox/lua-symbol 139fdfe6 git+https://github.rbx.com/roblox/lua-symbol#master",
"tutils tutils 0be577db git+https://github.rbx.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,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.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)
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,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,28 @@
local connect = require(script.connect)
local Provider = require(script.Provider)
local Logger = require(script.Logger)
local GetPolicyImplementations = script.getPolicyImplementations
local fromMemStorageService = require(GetPolicyImplementations.fromMemStorageService)
local fromPolicyService = require(GetPolicyImplementations.fromPolicyService)
local fromStaticSource = require(GetPolicyImplementations.fromStaticSource)
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