Koha Test Wiki MW Canasta on Koha Portainer

Test major Koha Wiki changes or bug fixes here without fear of breaking the production wiki.

For the current Koha Wiki, visit https://wiki.koha-community.org .

Module:Protection banner/testcases

From Koha Test Wiki MW Canasta on Koha Portainer
Jump to navigation Jump to search

Documentation for this module may be created at Module:Protection banner/testcases/doc

-- Load necessary modules
local mProtectionBanner = require('Module:Protection banner/sandbox')
local ScribuntoUnit = require('Module:ScribuntoUnit')

-- Get the classes
local Protection = mProtectionBanner._exportClasses().Protection
local Blurb = mProtectionBanner._exportClasses().Blurb
local BannerTemplate = mProtectionBanner._exportClasses().BannerTemplate
local Banner = mProtectionBanner._exportClasses().Banner
local Padlock = mProtectionBanner._exportClasses().Padlock

-- Initialise test suite
local suite = ScribuntoUnit:new()

--------------------------------------------------------------------------------
-- Default values
--------------------------------------------------------------------------------

local d = {}
d.reason = 'vandalism'
d.action = 'edit'
d.level = 'sysop'
d.page = 'User:Example'
d.talkPage = 'User talk:Example'
d.baseText = 'Example'
d.namespace = 2 -- Namespace of d.page
d.namespaceFragment = 'user' -- namespace fragment of d.page for makeProtectionCategory
d.categoryNamespaceKeys = {[d.namespace] = d.namespaceFragment} -- namespace key config table
d.expiry = '1 January 9999'
d.expiryU = 253370764800 -- d.expiry in Unix time
d.expiryFragment = 'temp' -- expiry fragment of d.expiry for makeProtectionCategory
d.protectionDate = '1 January 2000'
d.protectionDateU = 946684800 -- d.protectionDate in Unix time

--------------------------------------------------------------------------------
-- Helper functions
--------------------------------------------------------------------------------

function suite:assertError(func, args, msg)
	args = args or {}
	local success, result = pcall(func, unpack(args))
	self:assertFalse(success)
	if msg then
		self:assertStringContains(msg, result, true) -- does a plain match
	end
end

function suite:assertNotError(func, args)
	args = args or {}
	local success, result = pcall(func, unpack(args))
	self:assertTrue(success)
end

local function makeTitleObject(page, action, level)
	local levels = {
		edit = {},
		move = {},
		autoreview = {}
	}
	levels[action][1] = level
	local title = mw.title.new(page)
	return rawset(title, 'protectionLevels', levels)
end

local function makeDefaultTitleObject()
	return makeTitleObject(d.page, d.action, d.level)
end

-- Make an alias, to be clear the object is for a protected page.
local makeProtectedTitleObject = makeDefaultTitleObject

local function makeConfig(t)
	local cfg = {
		masterBanner = {},
		padlockIndicatorNames = {},
		indefImageReasons = {},
		reasonsWithNamespacePriority = {},
		categoryNamespaceKeys = {},
		protectionCategories = {},
		reasonsWithoutExpiryCheck = {},
		expiryCheckActions = {},
		pagetypes = {},
		indefStrings = {},
		wrappers = {},
		msg = {}
	}
	local protLevelFields = {
		'defaultBanners',
		'banners',
		'protectionBlurbs',
		'explanationBlurbs',
		'protectionLevels',
		'images',
		'imageLinks'
	}
	for _, field in ipairs(protLevelFields) do
		cfg[field] = {
			edit = {},
			move = {},
			autoreview = {}
		}
	end

	-- Add fields that cause errors if not present
	cfg.masterBanner.text = 'reason text'

	-- Add custom fields
	for k, v in pairs(t or {}) do
		cfg[k] = v
	end
	return cfg
end

local function makeDefaultProtectionObject(cfg)
	cfg = makeConfig(cfg)
	if next(cfg.categoryNamespaceKeys) == nil then -- cfg.categoryNamespaceKeys is empty
		cfg.categoryNamespaceKeys = d.categoryNamespaceKeys
	end
	local obj = Protection.new(
		{d.reason, action = d.action, expiry = d.expiry},
		cfg,
		makeDefaultTitleObject()
	)
	obj.expiry = d.expiryU -- Hack to override [[Module:Effective protection expiry]]
	return obj
end

local function makeProtectionCategoryKey(testFragments, defaults)
	local fragments = {
		expiry = 'all',
		namespace = 'all',
		reason = 'all',
		level = 'all',
		action = 'all'
	}
	for i, t in ipairs{defaults or {}, testFragments} do
		for k, v in pairs(t) do
			fragments[k] = v
		end
	end
	local key = {
		fragments.expiry,
		fragments.namespace,
		fragments.reason,
		fragments.level,
		fragments.action
	}
	return table.concat(key, '|')
end

local function makeDefaultProtectionCategoryKey(testFragments)
	local defaults = {
		expiry = d.expiryFragment,
		namespace = d.namespaceFragment,
		reason = d.reason,
		level = d.level,
		action = d.action
	}
	return makeProtectionCategoryKey(testFragments, defaults)
end

local function makeDefaultBlurbObject(cfg)
	cfg = makeConfig(cfg)
	return Blurb.new(
		makeDefaultProtectionObject(),
		{},
		cfg
	)
end

local function makeDefaultBannerTemplateObject(cfg)
	cfg = makeConfig(cfg)
	return BannerTemplate.new(
		makeDefaultProtectionObject(),
		cfg
	)
end

local function makeDefaultBannerObject(cfg)
	cfg = makeConfig(cfg)
	return Banner.new(
		makeDefaultProtectionObject(),
		makeDefaultBlurbObject(),
		cfg
	)
end

local function makeDefaultPadlockObject(cfg)
	cfg = makeConfig(cfg)
	return Padlock.new(
		makeDefaultProtectionObject(),
		makeDefaultBlurbObject(),
		cfg
	)
end

function suite:assertIsPadlock(s, msg)
	self:assertStringContains(
		'^%cUNIQ.-QINU%c$',
		s,
		false,
		msg
	)
end

function suite:assertIsBanner(s, msg)
	self:assertStringContains(
		'class="[^"]*mbox[^"]*"',
		s,
		false,
		msg
	)
	self:assertStringContains(
		'role="presentation"',
		s,
		true,
		msg
	)
	self:assertNotStringContains(
		'id="protected-icon"',
		s,
		true,
		msg
	)
	self:assertNotStringContains(
		'topicon',
		s,
		true,
		msg
	)
end

function suite:assertNoBanner(s, msg)
	self:assertNotStringContains(
		'class="[^"]*mbox[^"]*"',
		s,
		false,
		msg
	)
	self:assertNotStringContains(
		'role="presentation"',
		s,
		true,
		msg
	)
	self:assertNotStringContains(
		'id="protected-icon"',
		s,
		true,
		msg
	)
	self:assertNotStringContains(
		'topicon',
		s,
		true,
		msg
	)
end

--------------------------------------------------------------------------------
-- Protection object tests
--------------------------------------------------------------------------------

-- Protection action

function suite:testProtectionActionError()
	suite:assertError(
		Protection.new,
		{{action = 'foo'}, makeConfig()},
		'invalid action: foo'
	)
end

function suite:testProtectionActionEdit()
	local obj = Protection.new({action = 'edit'}, makeConfig())
	suite:assertEquals('edit', obj.action)
end

function suite:testProtectionActionMove()
	local obj = Protection.new({action = 'move'}, makeConfig())
	suite:assertEquals('move', obj.action)
end

function suite:testProtectionActionAutoreview()
	local obj = Protection.new({action = 'autoreview'}, makeConfig())
	suite:assertEquals('autoreview', obj.action)
end

function suite:testProtectionActionUpload()
	local obj = Protection.new({action = 'upload'}, makeConfig())
	suite:assertEquals('upload', obj.action)
end

function suite:testProtectionNoAction()
	local obj = Protection.new({}, makeConfig())
	suite:assertEquals('edit', obj.action)
end

-- Protection level

function suite:testProtectionSemi()
	local obj = Protection.new(
		{action = 'edit'},
		makeConfig(),
		makeTitleObject('Foo', 'edit', 'autoconfirmed')
	)
	self:assertEquals('autoconfirmed', obj.level)
end

function suite:testProtectionFull()
	local obj = Protection.new(
		{action = 'edit'},
		makeConfig(),
		makeTitleObject('Foo', 'edit', 'sysop')
	)
	self:assertEquals('sysop', obj.level)
end

function suite:testProtectionUnprotected()
	local obj = Protection.new(
		{action = 'edit'},
		makeConfig(),
		makeTitleObject('Foo', 'edit', nil)
	)
	self:assertEquals('*', obj.level)
end

function suite:testProtectionSemiMove()
	local obj = Protection.new(
		{action = 'move'},
		makeConfig(),
		makeTitleObject('Foo', 'move', 'autoconfirmed')
	)
	self:assertEquals('*', obj.level)
end

function suite:testProtectionTemplate()
	local obj = Protection.new(
		{action = 'edit'},
		makeConfig(),
		makeTitleObject('Template:Foo', 'edit', 'templateeditor')
	)
	self:assertEquals('templateeditor', obj.level)
end

function suite:testProtectionTitleBlacklist()
	local obj = Protection.new(
		{action = 'edit'},
		makeConfig(),
		makeTitleObject('Template:Editnotices/Page/Foo', 'edit', nil)
	)
	self:assertEquals('templateeditor', obj.level)
end

-- Expiry

function suite:testProtectionExpiryIndef()
	local obj = Protection.new(
		{expiry = 'indefinite'},
		makeConfig{indefStrings = {indefinite = true}}
	)
	self:assertEquals('indef', obj.expiry)
end

function suite:testProtectionExpiryTemp()
	local obj = Protection.new(
		{expiry = d.expiry,  action = 'autoreview' },
		makeConfig()
	)
	self:assertEquals(d.expiryU, obj.expiry)
end

function suite:testProtectionNoExpiry()
	local obj = Protection.new(
		{ action = 'autoreview' },
		makeConfig()
	)
	self:assertEquals(nil, obj.expiry)
end

function suite:testProtectionBadExpiry()
	self:assertError(
		Protection.new,
		{{expiry = 'foobar', action = 'autoreview' }, makeConfig()},
		'invalid expiry date: foobar'
	)
end

function suite:testProtectionPastExpiry()
	local obj = Protection.new(
		{expiry = d.protectionDate, action = 'autoreview' },
		makeConfig()
	)
	self:assertEquals(d.protectionDateU, obj.expiry)
end

-- Reason

function suite:testProtectionReason()
	local obj = Protection.new(
		{'foo'},
		makeConfig()
	)
	self:assertEquals('foo', obj.reason)
end

function suite:testProtectionReasonLowerCase()
	local obj = Protection.new(
		{'fOO'},
		makeConfig()
	)
	self:assertEquals('foo', obj.reason)
end

function suite:testProtectionBadReason()
	self:assertError(
		Protection.new,
		{{'foo|bar'}, makeConfig()},
		'reasons cannot contain the pipe character ("|")'
	)
end

function suite:testProtectionNoReason()
	local obj = Protection.new(
		{},
		makeConfig()
	)
	self:assertEquals(nil, obj.reason)
end

-- Protection date

function suite:testProtectionProtectionDateIndef()
	self:assertError(
		Protection.new,
		{
			{date = 'indefinite'},
			makeConfig{indefStrings = {indefinite = true}}
		},
		'invalid protection date: indefinite'
	)
end

function suite:testProtectionProtectionDateTemp()
	local obj = Protection.new(
		{date = d.protectionDate},
		makeConfig()
	)
	self:assertEquals(d.protectionDateU, obj.protectionDate)
end

function suite:testProtectionNoProtectionDate()
	local obj = Protection.new(
		{},
		makeConfig()
	)
	self:assertEquals(nil, obj.protectionDate)
end

function suite:testProtectionBadProtectionDate()
	self:assertError(
		Protection.new,
		{
			{date = 'foobar'},
			makeConfig{indefStrings = {indefinite = true}}
		},
		'invalid protection date: foobar'
	)
end

-- bannerConfig

function suite:testProtectionMasterBannerConfigPrecedence1()
	local masterBanner = {text = 'master banner text'}
	local obj = makeDefaultProtectionObject{masterBanner = masterBanner}
	self:assertEquals('master banner text', obj.bannerConfig.text)
end

function suite:testProtectionMasterBannerConfigPrecedence2()
	local masterBanner = {text = 'master banner text'}
	local defaultBanners = {[d.action] = {default = {text = 'defaultBanners default text'}}}
	local obj = makeDefaultProtectionObject(makeConfig{
		masterBanner = masterBanner,
		defaultBanners = defaultBanners
	})
	self:assertEquals('defaultBanners default text', obj.bannerConfig.text)
end

function suite:testProtectionMasterBannerConfigPrecedence3()
	local defaultBanners = {[d.action] = {
		[d.level] = {text = 'defaultBanners level text'},
		default = {text = 'defaultBanners default text'}
	}}
	local obj = makeDefaultProtectionObject(makeConfig{defaultBanners = defaultBanners})
	self:assertEquals('defaultBanners level text', obj.bannerConfig.text)
end

function suite:testProtectionMasterBannerConfigPrecedence4()
	local defaultBanners = {[d.action] = {
		[d.level] = {text = 'defaultBanners level text'}
	}}
	local banners = {[d.action] ={
		[d.reason] = {text = 'banners text'}
	}}
	local obj = makeDefaultProtectionObject(makeConfig{
		defaultBanners = defaultBanners,
		banners = banners
	})
	self:assertEquals('banners text', obj.bannerConfig.text)
end

function suite:testProtectionMasterBannerConfigFields()
	local masterBanner = {
		text = 'master banner text',
		explanation = 'master banner explanation',
		tooltip = 'master banner tooltip',
		alt = 'master banner alt',
		link = 'master banner link',
		image = 'master banner image'
	}
	local obj = makeDefaultProtectionObject{masterBanner = masterBanner}
	self:assertEquals('master banner text', obj.bannerConfig.text)
	self:assertEquals('master banner explanation', obj.bannerConfig.explanation)
	self:assertEquals('master banner tooltip', obj.bannerConfig.tooltip)
	self:assertEquals('master banner alt', obj.bannerConfig.alt)
	self:assertEquals('master banner link', obj.bannerConfig.link)
	self:assertEquals('master banner image', obj.bannerConfig.image)
end

-- isProtected

function suite:testProtectionIsProtectedTrue()
	local obj = makeDefaultProtectionObject()
	obj.level = 'autoconfirmed'
	self:assertTrue(obj:isProtected())
end

function suite:testProtectionIsProtectedFalse()
	local obj = makeDefaultProtectionObject()
	obj.level = '*'
	self:assertFalse(obj:isProtected())
end

-- isTemporary

function suite:testProtectionIsProtectedTrue()
	local obj = makeDefaultProtectionObject()
	obj.expiry = 123456789012
	self:assertTrue(obj:isTemporary())
end

function suite:testProtectionIsProtectedFalse1()
	local obj = makeDefaultProtectionObject()
	obj.expiry = 'indef'
	self:assertFalse(obj:isTemporary())
end

function suite:testProtectionIsProtectedFalse2()
	local obj = makeDefaultProtectionObject()
	obj.expiry = nil
	self:assertFalse(obj:isTemporary())
end

-- makeProtectionCategory

function suite:testProtectionCategoryPrecedence1()
	-- Test that expiry has the lowest priority.
	local protectionCategories = {
		[makeDefaultProtectionCategoryKey{expiry = 'all'}] = 'all expiries allowed',
		[makeDefaultProtectionCategoryKey{namespace = 'all'}] = 'all namespaces allowed',
		[makeDefaultProtectionCategoryKey{reason = 'all'}] = 'all reasons allowed',
		[makeDefaultProtectionCategoryKey{level = 'all'}] = 'all levels allowed',
		[makeDefaultProtectionCategoryKey{action = 'all'}] = 'all actions allowed'
	}
	local obj = makeDefaultProtectionObject{
		protectionCategories = protectionCategories,
	}
	self:assertStringContains(
		'all expiries allowed',
		obj:makeProtectionCategory(),
		true
	)
end

function suite:testProtectionCategoryPrecedence2()
	-- Test that reason has the highest priority.
	local protectionCategories = {
		[makeProtectionCategoryKey{expiry = d.expiryFragment}] = 'expiry only',
		[makeProtectionCategoryKey{namespace = d.namespaceFragment}] = 'namespace only',
		[makeProtectionCategoryKey{reason = d.reason}] = 'reason only',
		[makeProtectionCategoryKey{level = d.level}] = 'level only',
		[makeProtectionCategoryKey{action = d.action}] = 'action only'
	}
	local obj = makeDefaultProtectionObject{
		protectionCategories = protectionCategories,
	}
	self:assertStringContains(
		'reason only',
		obj:makeProtectionCategory(),
		true
	)
end

function suite:testProtectionCategoryPrecedence3()
	-- Test that namespace has the highest priority if the reason is set in
	-- cfg.reasonsWithNamespacePriority.
	local protectionCategories = {
		[makeProtectionCategoryKey{expiry = d.expiryFragment}] = 'expiry only',
		[makeProtectionCategoryKey{namespace = d.namespaceFragment}] = 'namespace only',
		[makeProtectionCategoryKey{reason = d.reason}] = 'reason only',
		[makeProtectionCategoryKey{level = d.level}] = 'level only',
		[makeProtectionCategoryKey{action = d.action}] = 'action only'
	}
	local obj = makeDefaultProtectionObject{
		protectionCategories = protectionCategories,
		reasonsWithNamespacePriority = {[d.reason] = true}
	}
	self:assertStringContains(
		'namespace only',
		obj:makeProtectionCategory(),
		true
	)
end

function suite:testProtectionCategoryPrecedence4()
	-- Test that level has a higher priority than namespace.
	local protectionCategories = {
		[makeDefaultProtectionCategoryKey{namespace = 'all'}] = 'all namespaces allowed',
		[makeDefaultProtectionCategoryKey{level = 'all'}] = 'all levels allowed',
	}
	local obj = makeDefaultProtectionObject{
		protectionCategories = protectionCategories,
	}
	self:assertStringContains(
		'all namespaces allowed',
		obj:makeProtectionCategory(),
		true
	)
end

function suite:testProtectionCategoryPrecedence5()
	-- Test that action has a higher priority than level.
	local protectionCategories = {
		[makeDefaultProtectionCategoryKey{level = 'all'}] = 'all levels allowed',
		[makeDefaultProtectionCategoryKey{action = 'all'}] = 'all actions allowed',
	}
	local obj = makeDefaultProtectionObject{
		protectionCategories = protectionCategories,
	}
	self:assertStringContains(
		'all levels allowed',
		obj:makeProtectionCategory(),
		true
	)
end

function suite:testProtectionCategoryPrecedence6()
	-- Test that an exact match will be first.
	local protectionCategories = {
		[makeDefaultProtectionCategoryKey()] = 'exact match',
		[makeDefaultProtectionCategoryKey{action = 'all'}] = 'all actions allowed',
	}
	local obj = makeDefaultProtectionObject{
		protectionCategories = protectionCategories,
	}
	self:assertStringContains(
		'exact match',
		obj:makeProtectionCategory(),
		true
	)
end

function suite:testProtectionCategoryPrecedence7()
	-- Test that matching 4 keys will come before matching 3 keys
	local protectionCategories = {
		[makeDefaultProtectionCategoryKey{action = 'all'}] = 'four keys',
		[makeDefaultProtectionCategoryKey{action = 'all', level = 'all'}] = 'three keys',
	}
	local obj = makeDefaultProtectionObject{
		protectionCategories = protectionCategories,
	}
	self:assertStringContains(
		'four keys',
		obj:makeProtectionCategory(),
		true
	)
end

function suite:testProtectionCategoryPrecedence8()
	-- Test that matching 3 keys will come before matching 2 keys
	local protectionCategories = {
		[makeProtectionCategoryKey{reason = d.reason, action = d.action, level = d.level}] = 'three keys',
		[makeProtectionCategoryKey{reason = d.reason, action = d.action}] = 'two keys',
	}
	local obj = makeDefaultProtectionObject{
		protectionCategories = protectionCategories,
	}
	self:assertStringContains(
		'three keys',
		obj:makeProtectionCategory(),
		true
	)
end

function suite:testProtectionCategoryPrecedence9()
	-- Test that matching 2 keys will come before matching 1 key
	local protectionCategories = {
		[makeProtectionCategoryKey{action = d.action, level = d.level}] = 'two keys',
		[makeProtectionCategoryKey{action = d.action}] = 'one key',
	}
	local obj = makeDefaultProtectionObject{
		protectionCategories = protectionCategories,
	}
	self:assertStringContains(
		'two keys',
		obj:makeProtectionCategory(),
		true
	)
end

function suite:testProtectionCategoryPrecedence10()
	-- Test that matching 1 keys will come before matching 0 keys
	local protectionCategories = {
		[makeProtectionCategoryKey{action = d.action}] = 'one key',
		[makeProtectionCategoryKey()] = 'no keys',
	}
	local obj = makeDefaultProtectionObject{
		protectionCategories = protectionCategories,
	}
	self:assertStringContains(
		'one key',
		obj:makeProtectionCategory(),
		true
	)
end

function suite:testProtectionCategoryAllAlls()
	-- Test that 'all|all|all|all|all' works
	local protectionCategories = {
		[makeProtectionCategoryKey()] = 'no keys',
	}
	local obj = makeDefaultProtectionObject{
		protectionCategories = protectionCategories,
	}
	self:assertStringContains(
		'no keys',
		obj:makeProtectionCategory(),
		true
	)
end

function suite:testProtectionCategoryNoFalseMatches()
	-- Test that we don't match things that we aren't supposed to.
	local protectionCategories = {
		[makeProtectionCategoryKey{expiry = 'foo'}]    = 'expiry foo',
		[makeProtectionCategoryKey{namespace = 'foo'}] = 'namespace foo',
		[makeProtectionCategoryKey{reason = 'foo'}]    = 'reason foo',
		[makeProtectionCategoryKey{level = 'foo'}]     = 'level foo',
		[makeProtectionCategoryKey{action = 'foo'}]    = 'action foo',
		[makeProtectionCategoryKey()]                  = 'no keys',
	}
	local obj = makeDefaultProtectionObject{
		protectionCategories = protectionCategories,
	}
	self:assertStringContains(
		'no keys',
		obj:makeProtectionCategory(),
		true
	)
end

function suite:testProtectionCategoryProtected()
	-- Test that protected pages produce some kind of category link.
	local protectionCategories = {
		[makeProtectionCategoryKey()] = 'no keys',
	}
	local obj = makeDefaultProtectionObject{
		protectionCategories = protectionCategories,
	}
	self:assertStringContains(
		'^%[%[Category:.*%]%]$',
		obj:makeProtectionCategory()
	)
end

function suite:testProtectionCategoryNotProtected()
	-- Test that unprotected pages produce the blank string.
	local protectionCategories = {
		[makeProtectionCategoryKey()] = 'no keys',
	}
	local obj = Protection.new(
		{},
		makeConfig{protectionCategories = protectionCategories},
		makeTitleObject(d.page, 'edit', nil)
	)
	self:assertEquals('', obj:makeProtectionCategory())
end

function suite:testProtectionCategoryNoMatch()
	-- Test that protected pages that don't match any categories
	-- produce the blank string.
	local obj = makeDefaultProtectionObject()
	self:assertEquals('', obj:makeProtectionCategory())
end

function suite:testProtectionCategoryExpiryIndef()
	-- Test that indefinite protection matches the "indef" expiry fragment.
	local obj = makeDefaultProtectionObject()
	obj._cfg.protectionCategories = {
		[makeProtectionCategoryKey{expiry = 'indef', action = 'autoreview'}] = 'indef expiry',
	}
	obj.action = 'autoreview'
	obj.level = 'autoconfirmed'
	obj.expiry = 'indef'
	self:assertStringContains('indef expiry', obj:makeProtectionCategory(), true)
end

function suite:testProtectionCategoryExpiryTemp()
	-- Test that temporary protection matches the "temp" expiry fragment.
	local obj = makeDefaultProtectionObject()
	obj._cfg.protectionCategories = {
		[makeProtectionCategoryKey{expiry = 'temp', action = 'autoreview'}] = 'temporary expiry',
	}
	obj.action = 'autoreview'
	obj.level = 'autoconfirmed'
	obj.expiry = d.expiryU
	self:assertStringContains('temporary expiry', obj:makeProtectionCategory(), true)
end

function suite:testProtectionCategoryNoExpiry()
	-- Test that pages with no expiry set don't match "indef" or "temp".
	local protectionCategories = {
		[makeProtectionCategoryKey{expiry = 'temp', action = 'autoreview' }] = 'temporary expiry',
		[makeProtectionCategoryKey{expiry = 'indef', action = 'autoreview' }] = 'indefinite expiry',
		[makeProtectionCategoryKey()] = 'no matches'
	}
	local obj = Protection.new(
		{},
		makeConfig{protectionCategories = protectionCategories},
		makeProtectedTitleObject()
	)
	self:assertStringContains('no matches', obj:makeProtectionCategory(), true)
end

function suite:testProtectionCategoryNamespaceFragment()
	-- Test that values in cfg.categoryNamespaceKeys will work.
	local protectionCategories = {
		[makeProtectionCategoryKey{namespace = 'foobar'}] = 'we found a match',
	}
	local obj = Protection.new(
		{},
		makeConfig{
			protectionCategories = protectionCategories,
			categoryNamespaceKeys = {[10] = 'foobar'} -- Template namespace
		},
		makeTitleObject('Template:Foo', 'edit', 'autoconfirmed')
	)
	self:assertStringContains('we found a match', obj:makeProtectionCategory(), true)
end

function suite:testProtectionCategoryTalk()
	-- Test that talk pages match the "talk" namespace fragment.
	local protectionCategories = {
		[makeProtectionCategoryKey{namespace = 'talk'}] = 'talk namespace',
	}
	local obj = Protection.new(
		{},
		makeConfig{protectionCategories = protectionCategories},
		makeTitleObject('Template talk:Example', 'edit', 'autoconfirmed')
	)
	self:assertStringContains('talk namespace', obj:makeProtectionCategory(), true)
end

-- needsExpiry

function suite:testProtectionNeedsExpiryFalse1()
	local obj = makeDefaultProtectionObject()
	obj.expiry = d.expiryU
	self:assertFalse(
		obj:needsExpiry(),
		'expiry is already set'
	)
end

function suite:testProtectionNeedsExpiryFalse2()
	local obj = makeDefaultProtectionObject()
	obj.expiry = nil
	obj.action = 'move'
	obj._cfg.expiryCheckActions.move = false
	self:assertFalse(
		obj:needsExpiry(),
		'action is blacklisted in cfg.expiryCheckActions'
	)
end

function suite:testProtectionNeedsExpiryFalse3()
	local obj = makeDefaultProtectionObject()
	obj.expiry = nil
	obj.action = 'move'
	obj._cfg.expiryCheckActions.move = nil
	obj.reason = nil
	self:assertFalse(
		obj:needsExpiry(),
		'cfg.expiryCheckActions is nil, and no reason is set'
	)
end

function suite:testProtectionNeedsExpiryFalse4()
	local obj = makeDefaultProtectionObject()
	obj.expiry = nil
	obj.action = 'move'
	obj._cfg.expiryCheckActions.move = nil
	obj.reason = 'vandalism'
	obj._cfg.reasonsWithoutExpiryCheck.vandalism = true
	self:assertFalse(
		obj:needsExpiry(),
		'cfg.expiryCheckActions is nil, and the reason is blacklisted'
	)
end

function suite:testProtectionNeedsExpiryTrue1()
	local obj = makeDefaultProtectionObject()
	obj.expiry = nil
	obj.action = 'move'
	obj._cfg.expiryCheckActions.move = true
	self:assertTrue(
		obj:needsExpiry(),
		'expiry is nil and action is whitelisted in cfg.expiryCheckActions'
	)
end

function suite:testProtectionNeedsExpiryTrue2()
	local obj = makeDefaultProtectionObject()
	obj.expiry = nil
	obj.action = 'move'
	obj._cfg.expiryCheckActions.move = nil
	obj.reason = 'vandalism'
	obj._cfg.reasonsWithoutExpiryCheck.vandalism = nil
	self:assertTrue(
		obj:needsExpiry(),
		'expiry and expiryCheckActions are nil, and the reason is present and'
			.. ' not blacklisted.'
	)
end

-- isIncorrect

function suite:testProtectionIsIncorrectTrue1()
	local obj = makeDefaultProtectionObject()
	function obj:isProtected()
		return false
	end
	self:assertTrue(obj:isIncorrect(), 'Protection:isProtected() returned false')
end

function suite:testProtectionIsIncorrectTrue2()
	local obj = makeDefaultProtectionObject()
	function obj:isProtected()
		return true
	end
	obj.expiry = 0
	self:assertTrue(obj:isIncorrect(), 'the page is protected and expiry is in the past')
end

function suite:testProtectionIsIncorrectFalse1()
	local obj = makeDefaultProtectionObject()
	function obj:isProtected()
		return true
	end
	obj.expiry = d.expiryU
	self:assertFalse(obj:isIncorrect(), 'the page is protected and expiry is in the future')
end

function suite:testProtectionIsIncorrectFalse2()
	local obj = makeDefaultProtectionObject()
	obj.expiry = nil
	function obj:isProtected()
		return true
	end
	self:assertFalse(obj:isIncorrect(), 'the page is protected and no expiry is set')
end

-- isTemplateProtectedNonTemplate

function suite:testProtectionIsTemplateProtectedNonTemplateFalse1()
	local obj = makeDefaultProtectionObject()
	obj.level = 'autoconfirmed'
	self:assertFalse(obj:isTemplateProtectedNonTemplate(), 'the page is semi-protected')
end

function suite:testProtectionIsTemplateProtectedNonTemplateFalse2()
	local obj = makeDefaultProtectionObject()
	obj.level = 'templateeditor'
	obj.action = 'edit'
	rawset(obj.title, 'namespace', 10) -- template space
	self:assertFalse(obj:isTemplateProtectedNonTemplate(), 'template-protected template')
end

function suite:testProtectionIsTemplateProtectedNonTemplateFalse3()
	local obj = makeDefaultProtectionObject()
	obj.level = 'templateeditor'
	obj.action = 'edit'
	rawset(obj.title, 'namespace', 828) -- module space
	self:assertFalse(obj:isTemplateProtectedNonTemplate(), 'template-protected module')
end

function suite:testProtectionIsTemplateProtectedNonTemplateFalse4()
	local obj = makeDefaultProtectionObject()
	obj.level = 'templatemoveor'
	obj.action = 'move'
	rawset(obj.title, 'namespace', 10) -- template space
	self:assertFalse(obj:isTemplateProtectedNonTemplate(), 'template-move-protected template')
end

function suite:testProtectionIsTemplateProtectedNonTemplateFalse5()
	local obj = makeDefaultProtectionObject()
	obj.level = 'templatemoveor'
	obj.action = 'move'
	rawset(obj.title, 'namespace', 828) -- module space
	self:assertFalse(obj:isTemplateProtectedNonTemplate(), 'template-move-protected module')
end

function suite:testProtectionIsTemplateProtectedNonTemplateTrue1()
	local obj = makeDefaultProtectionObject()
	obj.level = 'templateeditor'
	obj.action = 'autoreview'
	self:assertTrue(obj:isTemplateProtectedNonTemplate(), 'the action is not edit or move')
end

function suite:testProtectionIsTemplateProtectedNonTemplateTrue2()
	local obj = makeDefaultProtectionObject()
	obj.level = 'templateeditor'
	rawset(obj.title, 'namespace', 2) -- user space
	self:assertTrue(obj:isTemplateProtectedNonTemplate(), 'the action is not in template or module space')
end

-- makeCategoryLinks

function suite:testProtectionMakeCategoryLinksAllPresent()
	local obj = makeDefaultProtectionObject()
	obj._cfg.msg = {
		['tracking-category-expiry'] = 'Expiry category',
		['tracking-category-incorrect'] = 'Incorrect category',
		['tracking-category-template'] = 'Template category'
	}
	function obj:makeProtectionCategory()
		return '[[Category:Protection category|' .. d.baseText .. ']]'
	end
	for _, field in ipairs{'needsExpiry', 'isIncorrect', 'isTemplateProtectedNonTemplate'} do
		obj[field] = function () return true end
	end
	self:assertEquals(
		'[[Category:Protection category|' .. d.baseText .. ']]' ..
		'[[Category:Expiry category|' .. d.baseText .. ']]' ..
		'[[Category:Incorrect category|' .. d.baseText .. ']]' ..
		'[[Category:Template category|' .. d.baseText .. ']]',
		obj:makeCategoryLinks()
	)
end

function suite:testProtectionMakeCategoryLinksAllAbsent()
	local obj = makeDefaultProtectionObject()
	obj._cfg.msg = {
		['tracking-category-expiry'] = 'Expiry category',
		['tracking-category-incorrect'] = 'Incorrect category',
		['tracking-category-template'] = 'Template category'
	}
	function obj:makeProtectionCategory()
		return ''
	end
	for _, field in ipairs{'needsExpiry', 'isIncorrect', 'isTemplateProtectedNonTemplate'} do
		obj[field] = function () return false end
	end
	self:assertEquals('', obj:makeCategoryLinks())
end

--------------------------------------------------------------------------------
-- Blurb class tests
--------------------------------------------------------------------------------

-- initialize

function suite:testBlurbNew()
	local obj = Blurb.new({'foo'}, {'bar'}, {'baz'})
	self:assertEquals('foo', obj._protectionObj[1])
	self:assertEquals('bar', obj._args[1])
	self:assertEquals('baz', obj._cfg[1])
end

-- _formatDate

function suite:testBlurbFormatDateStandard()
	local obj = makeDefaultBlurbObject()
	self:assertEquals('1 January 1970', obj:_formatDate(0))
end

function suite:testBlurbFormatDateCustom()
	local obj = makeDefaultBlurbObject{msg = {['expiry-date-format'] = 'Y Y F F j'}}
	self:assertEquals('1970 1970 January January 1', obj:_formatDate(0))
end

function suite:testBlurbFormatDateError()
	local obj = makeDefaultBlurbObject()
	self:assertEquals(nil, obj:_formatDate('foo'))
end

-- _getExpandedMessage

function suite:testBlurbGetExpandedMessage()
	local obj = makeDefaultBlurbObject()
	function obj:_substituteParameters(s)
		return 'testing ' .. s
	end
	obj._cfg.msg = {['test-key'] = 'test message'}
	self:assertEquals('testing test message', obj:_getExpandedMessage('test-key'))
end

-- _substituteParameters

function suite:testBlurbSubstituteParameters()
	local obj = makeDefaultBlurbObject()

	obj._makeCurrentVersionParameter = function () return '1' end
	obj._makeEditRequestParameter = function () return '2' end
	obj._makeExpiryParameter = function () return '3' end
	obj._makeExplanationBlurbParameter = function () return '4' end
	obj._makeImageLinkParameter = function () return '5' end
	obj._makeIntroBlurbParameter = function () return '6' end
	obj._makeIntroFragmentParameter = function () return '7' end
	obj._makePagetypeParameter = function () return '8' end
	obj._makeProtectionBlurbParameter = function () return '9' end
	obj._makeProtectionDateParameter = function () return '10' end
	obj._makeProtectionLevelParameter = function () return '11' end
	obj._makeProtectionLogParameter = function () return '12' end
	obj._makeTalkPageParameter = function () return '13' end
	obj._makeTooltipBlurbParameter = function () return '14' end
	obj._makeTooltipFragmentParameter = function () return '15' end
	obj._makeVandalTemplateParameter = function () return '16' end

	local msg = '${CURRENTVERSION}-' ..
	'${EDITREQUEST}-' ..
	'${EXPIRY}-' ..
	'${EXPLANATIONBLURB}-' ..
	'${IMAGELINK}-' ..
	'${INTROBLURB}-' ..
	'${INTROFRAGMENT}-' ..
	'${PAGETYPE}-' ..
	'${PROTECTIONBLURB}-' ..
	'${PROTECTIONDATE}-' ..
	'${PROTECTIONLEVEL}-' ..
	'${PROTECTIONLOG}-' ..
	'${TALKPAGE}-' ..
	'${TOOLTIPBLURB}-' ..
	'${TOOLTIPFRAGMENT}-' ..
	'${VANDAL}'

	local expected = '1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-16'

	self:assertEquals(expected, obj:_substituteParameters(msg))
end
 
-- makeCurrentVersionParameter

function suite:testBlurbMakeCurrentVersionParameterEdit()
	local obj = makeDefaultBlurbObject()
	obj._protectionObj.action = 'edit'
	obj._cfg.msg['current-version-edit-display'] = 'edit display'
	self:assertEquals(
		'[//en.wikipedia.org/w/index.php?title='
			.. mw.uri.encode(d.page)
			.. '&action=history edit display]',
		obj:_makeCurrentVersionParameter()
	)
end

function suite:testBlurbMakeCurrentVersionParameterMove()
	local obj = makeDefaultBlurbObject()
	obj._protectionObj.action = 'move'
	obj._cfg.msg['current-version-move-display'] = 'move display'
	self:assertEquals(
		'[//en.wikipedia.org/w/index.php?title='
			.. mw.uri.encode('Special:Log')
			.. '&page='
			.. mw.uri.encode(d.page)
			.. '&type=move move display]',
		obj:_makeCurrentVersionParameter()
	)
end

-- _makeEditRequestParameter

function suite:testBlurbMakeEditRequestParameterLevels()
	-- We can't test the edit requests directly as that is the responsibility of
	-- [[Module:Submit an edit request]], but we can check that we get different
	-- outputs for different protection levels.
	local function makeBlurbObjectWithLevels(action, level)
		local obj = makeDefaultBlurbObject()
		obj._protectionObj.action = action
		obj._protectionObj.level = level
		obj._cfg.msg['edit-request-display'] = 'display'
		return obj
	end
	local obj1 = makeBlurbObjectWithLevels('edit', 'autoconfirmed')
	local obj2 = makeBlurbObjectWithLevels('edit', 'templateeditor')
	local obj3 = makeBlurbObjectWithLevels('edit', 'sysop')
	local obj4 = makeBlurbObjectWithLevels('move', 'templateeditor')

	self:assertFalse(obj1:_makeEditRequestParameter() == obj2:_makeEditRequestParameter())
	self:assertFalse(obj2:_makeEditRequestParameter() == obj3:_makeEditRequestParameter())
	self:assertEquals(obj3:_makeEditRequestParameter(), obj4:_makeEditRequestParameter())
end

function suite:testBlurbMakeEditRequestParameterLink()
	-- Check that the edit request links have features that we can always expect
	-- to be there. The rest is subject to be changed by [[Module:Submit an edit request]]
	-- at any time, so we won't test that here.
	local obj = makeDefaultBlurbObject()
	obj._protectionObj.action = 'edit'
	obj._protectionObj.level = 'autoconfirmed'
	obj._cfg.msg['edit-request-display'] = 'the edit request display'
	self:assertStringContains(
		'//en.wikipedia.org/w/index.php?',
		obj:_makeEditRequestParameter(),
		true
	)
	self:assertStringContains(
		'action=edit',
		obj:_makeEditRequestParameter(),
		true
	)
	self:assertStringContains(
		'title=[a-zA-Z0-9%%_]*[tT]alk',
		obj:_makeEditRequestParameter(),
		false
	)
	self:assertStringContains(
		'the edit request display',
		obj:_makeEditRequestParameter(),
		true
	)
end

-- _makeExpiryParameter

function suite:testBlurbMakeExpiryParameterTemp()
	local obj = makeDefaultBlurbObject()
	obj._protectionObj.expiry = 0
	function obj:_formatDate(num)
		return 'unix date is ' .. tostring(num)
	end
	self:assertEquals('unix date is 0', obj:_makeExpiryParameter())
end

function suite:testBlurbMakeExpiryParameterOther()
	local obj = makeDefaultBlurbObject()
	obj._protectionObj.expiry = 'indef'
	function obj:_formatDate(num)
		return 'unix date is ' .. tostring(num)
	end
	self:assertEquals('indef', obj:_makeExpiryParameter())
end

-- _makeExplanationBlurbParameter

function suite:testBlurbMakeExplanationBlurbParameter()
	local obj = makeDefaultBlurbObject()
	obj._protectionObj.action = 'edit'
	obj._protectionObj.level = 'autoconfirmed'
	rawset(obj._protectionObj.title, 'isTalkPage', true)

	obj._cfg.explanationBlurbs = {
		edit = {
			autoconfirmed = {
				talk = 'edit-autoconfirmed-talk',
				default = 'edit-autoconfirmed-default'
			},
			default = {
				talk = 'edit-default-talk',
				default = 'edit-default-default'
			}
		}
	}

	function obj:_substituteParameters(msg)
		return msg
	end

	self:assertEquals(
		'edit-autoconfirmed-talk',
		obj:_makeExplanationBlurbParameter()
	)

	obj._cfg.explanationBlurbs.edit.autoconfirmed.talk = nil
	self:assertEquals(
		'edit-autoconfirmed-default',
		obj:_makeExplanationBlurbParameter()
	)

	obj._cfg.explanationBlurbs.edit.autoconfirmed.default = nil
	self:assertEquals(
		'edit-default-talk',
		obj:_makeExplanationBlurbParameter()
	)

	obj._cfg.explanationBlurbs.edit.default.talk = nil
	self:assertEquals(
		'edit-default-default',
		obj:_makeExplanationBlurbParameter()
	)

	obj._cfg.explanationBlurbs.edit.default.default = nil
	self:assertError(
		obj._makeExplanationBlurbParameter,
		{obj},
		'could not find explanation blurb for action "edit",'
			.. ' level "autoconfirmed" and talk key "talk"'
	)
end

function suite:testBlurbMakeExplanationBlurbParameterSpecialCases()
	local obj = makeDefaultBlurbObject()
	rawset(obj._protectionObj.title, 'namespace', 8)
	function obj:_getExpandedMessage(key)
		return 'the key is ' .. key
	end
	self:assertEquals(
		'the key is explanation-blurb-nounprotect',
		obj:_makeExplanationBlurbParameter()
	)
end

-- _makeImageLinkParameter

function suite:testBlurbMakeImageLinkParameter()
	local obj = makeDefaultBlurbObject()
	obj._protectionObj.action = 'move'
	obj._protectionObj.level = 'sysop'

	obj._cfg.imageLinks = {
		edit = {
			sysop = 'edit-sysop',
			default = 'edit-default'
		},
		move = {
			sysop = 'move-sysop',
			default = 'move-default'
		}
	}

	function obj:_substituteParameters(msg)
		return msg
	end

	self:assertEquals(
		'move-sysop',
		obj:_makeImageLinkParameter()
	)

	obj._cfg.imageLinks.move.sysop = nil
	self:assertEquals(
		'move-default',
		obj:_makeImageLinkParameter()
	)

	obj._cfg.imageLinks.move.default = nil
	self:assertEquals(
		'edit-default',
		obj:_makeImageLinkParameter()
	)
end

-- _makeIntroBlurbParameter

function suite:testBlurbMakeIntroBlurbParameter()
	local obj = makeDefaultBlurbObject()
	function obj._protectionObj:isTemporary()
		return true
	end
	function obj:_getExpandedMessage(key)
		return 'the key is ' .. key
	end
	self:assertEquals(
		'the key is intro-blurb-expiry',
		obj:_makeIntroBlurbParameter()
	)

	function obj._protectionObj:isTemporary()
		return false
	end
	self:assertEquals(
		'the key is intro-blurb-noexpiry',
		obj:_makeIntroBlurbParameter()
	)
end

-- _makeIntroFragmentParameter

function suite:testBlurbMakeIntroFragmentParameter()
	local obj = makeDefaultBlurbObject()
	function obj._protectionObj:isTemporary()
		return true
	end
	function obj:_getExpandedMessage(key)
		return 'the key is ' .. key
	end
	self:assertEquals(
		'the key is intro-fragment-expiry',
		obj:_makeIntroFragmentParameter()
	)

	function obj._protectionObj:isTemporary()
		return false
	end
	self:assertEquals(
		'the key is intro-fragment-noexpiry',
		obj:_makeIntroFragmentParameter()
	)
end

-- _makePagetypeParameter

function suite:testPagetypeParameter()
	local obj = makeDefaultBlurbObject()
	rawset(obj._protectionObj.title, 'namespace', 3)

	obj._cfg.pagetypes = {
		[3] = 'user talk page',
		default = 'default page'
	}

	self:assertEquals(
		'user talk page',
		obj:_makePagetypeParameter()
	)

	obj._cfg.pagetypes[3] = nil
	self:assertEquals(
		'default page',
		obj:_makePagetypeParameter()
	)

	obj._cfg.pagetypes.default = nil
	self:assertError(
		obj._makePagetypeParameter,
		{obj},
		'no default pagetype defined'
	)
end

-- _makeProtectionBlurbParameter

function suite:testBlurbMakeProtectionBlurbParameter()
	local obj = makeDefaultBlurbObject()
	obj._protectionObj.action = 'move'
	obj._protectionObj.level = 'sysop'

	obj._cfg.protectionBlurbs = {
		edit = {
			sysop = 'edit-sysop',
			default = 'edit-default'
		},
		move = {
			sysop = 'move-sysop',
			default = 'move-default'
		}
	}

	function obj:_substituteParameters(msg)
		return msg
	end

	self:assertEquals(
		'move-sysop',
		obj:_makeProtectionBlurbParameter()
	)

	obj._cfg.protectionBlurbs.move.sysop = nil
	self:assertEquals(
		'move-default',
		obj:_makeProtectionBlurbParameter()
	)

	obj._cfg.protectionBlurbs.move.default = nil
	self:assertEquals(
		'edit-default',
		obj:_makeProtectionBlurbParameter()
	)

	obj._cfg.protectionBlurbs.edit.default = nil
	self:assertError(
		obj._makeProtectionBlurbParameter,
		{obj},
		'no protection blurb defined for protectionBlurbs.edit.default'
	)
end

-- _makeProtectionDateParameter

function suite:testBlurbMakeProtectionDateParameter()
	local obj = makeDefaultBlurbObject()
	obj._protectionObj.protectionDate = 0
	function obj:_formatDate(num)
		return 'unix date is ' .. tostring(num)
	end
	self:assertEquals('unix date is 0', obj:_makeProtectionDateParameter())

	obj._protectionObj.protectionDate = 'indef'
	self:assertEquals('indef', obj:_makeProtectionDateParameter())
end

-- _makeProtectionLevelParameter

function suite:testBlurbMakeProtectionLevelParameter()
	local obj = makeDefaultBlurbObject()
	obj._protectionObj.action = 'move'
	obj._protectionObj.level = 'sysop'

	obj._cfg.protectionLevels = {
		edit = {
			sysop = 'edit-sysop',
			default = 'edit-default'
		},
		move = {
			sysop = 'move-sysop',
			default = 'move-default'
		}
	}

	function obj:_substituteParameters(msg)
		return msg
	end

	self:assertEquals(
		'move-sysop',
		obj:_makeProtectionLevelParameter()
	)

	obj._cfg.protectionLevels.move.sysop = nil
	self:assertEquals(
		'move-default',
		obj:_makeProtectionLevelParameter()
	)

	obj._cfg.protectionLevels.move.default = nil
	self:assertEquals(
		'edit-default',
		obj:_makeProtectionLevelParameter()
	)

	obj._cfg.protectionLevels.edit.default = nil
	self:assertError(
		obj._makeProtectionLevelParameter,
		{obj},
		'no protection level defined for protectionLevels.edit.default'
	)
end

-- _makeProtectionLogParameter

function suite:testBlurbMakeProtectionLogParameterPC()
	local obj = makeDefaultBlurbObject()
	obj._protectionObj.action = 'autoreview'
	function obj:_getExpandedMessage(key)
		return 'the key is ' .. key
	end
	self:assertStringContains(
		'^%[//en%.wikipedia%.org/w/index%.php?',
		obj:_makeProtectionLogParameter(),
		false
	)
	self:assertStringContains(
		'title=' .. mw.uri.encode('Special:Log'),
		obj:_makeProtectionLogParameter(),
		true
	)
	self:assertStringContains(
		'type=stable',
		obj:_makeProtectionLogParameter(),
		true
	)
	self:assertStringContains(
		'page=' .. mw.uri.encode(d.page),
		obj:_makeProtectionLogParameter(),
		true
	)
	self:assertStringContains(
		'the key is pc%-log%-display%]$',
		obj:_makeProtectionLogParameter(),
		false
	)
end

function suite:testBlurbMakeProtectionLogParameterProtection()
	local obj = makeDefaultBlurbObject()
	obj._protectionObj.action = 'edit'
	function obj:_getExpandedMessage(key)
		return 'the key is ' .. key
	end
	self:assertStringContains(
		'^%[//en%.wikipedia%.org/w/index%.php?',
		obj:_makeProtectionLogParameter(),
		false
	)
	self:assertStringContains(
		'title=' .. mw.uri.encode('Special:Log'),
		obj:_makeProtectionLogParameter(),
		true
	)
	self:assertStringContains(
		'type=protect',
		obj:_makeProtectionLogParameter(),
		true
	)
	self:assertStringContains(
		'page=' .. mw.uri.encode(d.page),
		obj:_makeProtectionLogParameter(),
		true
	)
	self:assertStringContains(
		'the key is protection%-log%-display%]$',
		obj:_makeProtectionLogParameter(),
		false
	)
end

-- _makeTalkPageParameter

function suite:testBlurbMakeTalkPageParameter()
	local obj = makeDefaultBlurbObject()
	function obj:_getExpandedMessage(key)
		return 'the key is ' .. key
	end
	self:assertEquals(
		'[['
			.. d.talkPage .. '#top|'
			.. 'the key is talk-page-link-display'
			.. ']]',
		obj:_makeTalkPageParameter()
	)
	obj._args.section = 'talk section'
	self:assertEquals(
		'[['
			.. d.talkPage .. '#talk section|'
			.. 'the key is talk-page-link-display'
			.. ']]',
		obj:_makeTalkPageParameter()
	)
end

-- _makeTooltipBlurbParameter

function suite:testBlurbMakeTooltipBlurbParameter()
	local obj = makeDefaultBlurbObject()
	function obj._protectionObj:isTemporary()
		return true
	end
	function obj:_getExpandedMessage(key)
		return 'the key is ' .. key
	end
	self:assertEquals(
		'the key is tooltip-blurb-expiry',
		obj:_makeTooltipBlurbParameter()
	)

	function obj._protectionObj:isTemporary()
		return false
	end
	self:assertEquals(
		'the key is tooltip-blurb-noexpiry',
		obj:_makeTooltipBlurbParameter()
	)
end

-- _makeTooltipFragmentParameter

function suite:testBlurbMakeTooltipFragmentParameter()
	local obj = makeDefaultBlurbObject()
	function obj._protectionObj:isTemporary()
		return true
	end
	function obj:_getExpandedMessage(key)
		return 'the key is ' .. key
	end
	self:assertEquals(
		'the key is tooltip-fragment-expiry',
		obj:_makeTooltipFragmentParameter()
	)

	function obj._protectionObj:isTemporary()
		return false
	end
	self:assertEquals(
		'the key is tooltip-fragment-noexpiry',
		obj:_makeTooltipFragmentParameter()
	)
end

-- _makeVandalTemplateParameter

function suite:testBlurbMakeVandalTemplateParameter()
	local obj = makeDefaultBlurbObject()
	self:assertStringContains(
		d.baseText,
		obj:_makeVandalTemplateParameter(),
		true
	)
	obj._args.user = 'Some user'
	self:assertStringContains(
		'Some user',
		obj:_makeVandalTemplateParameter(),
		true
	)
end

-- makeBannerText

function suite:testBlurbMakeBannerTextBadInput()
	local obj = makeDefaultBlurbObject()
	self:assertError(
		obj.makeBannerText,
		{obj, 'foo'},
		'"foo" is not a valid banner config field'
	)
	self:assertError(
		obj.makeBannerText,
		{obj, nil},
		'"nil" is not a valid banner config field'
	)
end

function suite:testBlurbMakeBannerTextGoodInput()
	local obj = makeDefaultBlurbObject()
	obj._protectionObj.bannerConfig = {
		text = 'banner text',
		explanation = 'banner explanation',
		tooltip = 'banner tooltip',
		alt = 'banner alt',
		link = 'banner link'
	}
	self:assertNotError(obj.makeBannerText, {obj, 'text'})
	self:assertNotError(obj.makeBannerText, {obj, 'explanation'})
	self:assertNotError(obj.makeBannerText, {obj, 'tooltip'})
	self:assertNotError(obj.makeBannerText, {obj, 'alt'})
	self:assertNotError(obj.makeBannerText, {obj, 'link'})
end

function suite:testBlurbMakeBannerTextString()
	local obj = makeDefaultBlurbObject()
	function obj:_substituteParameters(msg)
		return msg
	end
	obj._protectionObj.bannerConfig = {
		text = 'banner text',
	}
	self:assertEquals('banner text', obj:makeBannerText('text'))
end

function suite:testBlurbMakeBannerTextBadFunction()
	local obj = makeDefaultBlurbObject()
	function obj:_substituteParameters(msg)
		return msg
	end
	obj._protectionObj.bannerConfig = {
		text = function () return 9 end,
	}
	self:assertError(
		obj.makeBannerText,
		{obj, 'text'},
		'bad output from banner config function with key "text"'
			.. ' (expected string, got number)'
	)
end

function suite:testBlurbMakeBannerTextGoodFunction()
	local obj = makeDefaultBlurbObject()
	function obj:_substituteParameters(msg)
		return msg
	end
	obj._protectionObj.bannerConfig = {
		text = function () return 'some text' end,
	}
	self:assertEquals('some text', obj:makeBannerText('text'))
end

--------------------------------------------------------------------------------
-- BannerTemplate class tests
--------------------------------------------------------------------------------

-- BannerTemplate.new

function suite:testBannerTemplateNewCfg()
	local protectionObj = makeDefaultProtectionObject()
	local obj = BannerTemplate.new(protectionObj, makeConfig{foo = 'bar'})
	self:assertEquals('bar', obj._cfg.foo)
end

function suite:testBannerTemplateNewImageIndefTemplateOrModule()
	local cfg = {
		msg = {['image-filename-indef'] = 'red padlock'}
	}
	local protectionObj = makeDefaultProtectionObject()
	protectionObj.action = 'edit'
	protectionObj.level = 'sysop'
	function protectionObj:isTemporary() return false end

	rawset(protectionObj.title, 'namespace', 10)
	local obj1 = BannerTemplate.new(protectionObj, makeConfig(cfg))
	self:assertEquals('red padlock', obj1._imageFilename)

	rawset(protectionObj.title, 'namespace', 828)
	local obj2 = BannerTemplate.new(protectionObj, makeConfig(cfg))
	self:assertEquals('red padlock', obj2._imageFilename)
end

function suite:testBannerTemplateNewImageUsesIndefReason()
	local cfg = {
		indefImageReasons = {[d.reason] = true},
		msg = {['image-filename-indef'] = 'red padlock'}
	}
	local protectionObj = makeDefaultProtectionObject()
	protectionObj.action = 'edit'
	protectionObj.level = 'sysop'
	function protectionObj:isTemporary() return false end
	rawset(protectionObj.title, 'namespace', 2)
	local obj = BannerTemplate.new(protectionObj, makeConfig(cfg))
	self:assertEquals('red padlock', obj._imageFilename)
end		

function suite:testBannerTemplateNewImageDefault()
	local images = {
		move = {
			sysop = 'foo',
			default = 'bar'
		}
	}
	local protectionObj = makeDefaultProtectionObject()
	protectionObj.action = 'move'
	protectionObj.level = 'sysop'
	local obj = BannerTemplate.new(protectionObj, makeConfig{
		images = images
	})

	self:assertEquals('foo', obj._imageFilename)

	images.move.sysop = nil
	obj = BannerTemplate.new(protectionObj, makeConfig{
		images = images
	})
	self:assertEquals('bar', obj._imageFilename)

	images.move.default = nil
	obj = BannerTemplate.new(protectionObj, makeConfig{
		images = images
	})
	self:assertEquals(nil, obj._imageFilename)
end

-- renderImage

function suite:testBannerTemplateRenderImageFilename()
	local obj = makeDefaultBannerTemplateObject()
	obj._imageFilename = 'ImageFilename.png'
	self:assertStringContains('ImageFilename.png', obj:renderImage(), true)
end

function suite:testBannerTemplateRenderImageDefault()
	local obj = makeDefaultBannerTemplateObject()
	obj._cfg.msg['image-filename-default'] = 'Defaultfilename.png'
	self:assertStringContains('Defaultfilename.png', obj:renderImage(), true)
end

function suite:testBannerTemplateRenderImageDefaultNoConfig()
	local obj = makeDefaultBannerTemplateObject()
	self:assertStringContains('Transparent.gif', obj:renderImage(), true)
end

function suite:testBannerTemplateRenderImageDefaultWidth()
	local obj = makeDefaultBannerTemplateObject()
	self:assertStringContains('20px', obj:renderImage(), true)
end

function suite:testBannerTemplateRenderImageCustomWidth()
	local obj = makeDefaultBannerTemplateObject()
	obj.imageWidth = 50
	self:assertStringContains('50px', obj:renderImage(), true)
end

function suite:testBannerTemplateRenderImageAlt()
	local obj = makeDefaultBannerTemplateObject()
	obj._imageAlt = 'the alt text'
	self:assertStringContains('alt%s*=%s*the alt text', obj:renderImage(), false)
end

function suite:testBannerTemplateRenderImageLink()
	local obj = makeDefaultBannerTemplateObject()
	obj._imageLink = 'the link text'
	self:assertStringContains('link%s*=%s*the link text', obj:renderImage(), false)
end

function suite:testBannerTemplateRenderImageCaption()
	local obj = makeDefaultBannerTemplateObject()
	obj.imageCaption = 'the caption text'
	self:assertStringContains('the caption text', obj:renderImage(), true)
end

--------------------------------------------------------------------------------
-- Banner class tests
--------------------------------------------------------------------------------

function suite:testBannerNew()
	local protectionObj = makeDefaultProtectionObject()
	local blurbObj = makeDefaultBlurbObject()
	local cfg = makeConfig()

	function blurbObj:makeBannerText(key)
		if key == 'alt' then
			return 'the alt text'
		elseif key == 'text' then
			return 'the main text'
		elseif key == 'explanation' then
			return 'the explanation text'
		end
	end
	
	local obj = Banner.new(protectionObj, blurbObj, cfg)

	self:assertEquals(40, obj.imageWidth)
	self:assertEquals('the alt text', obj.imageCaption)
	self:assertEquals('the main text', obj._reasonText)
	self:assertEquals('the explanation text', obj._explanationText)
	self:assertEquals(d.page, obj._page)
end

-- __tostring

function suite:testBannerToStringError()
	local obj = makeDefaultBannerObject()
	obj._reasonText = nil
	self:assertError(obj.__tostring, {obj}, 'no reason text set')
end

function suite:testBannerToString()
	local obj = makeDefaultBannerObject()
	obj._reasonText = 'the reason text'
	obj._explanationText = 'the explanation text'
	
	function obj:renderImage()
		return '[[File:Example.png|30px]]'
	end

	self:assertStringContains('[[File:Example.png|30px]]', tostring(obj), true)
	self:assertStringContains(
		"'''the reason text'''<br />the explanation text",
		tostring(obj),
		true
	)

	obj._explanationText = nil
	self:assertStringContains("'''the reason text'''", tostring(obj), true)
end

--------------------------------------------------------------------------------
-- Padlock class tests
--------------------------------------------------------------------------------

function suite:testPadlockNew()
	local protectionObj = makeDefaultProtectionObject()
	local blurbObj = makeDefaultBlurbObject()
	local cfg = makeConfig()

	function blurbObj:makeBannerText(key)
		if key == 'alt' then
			return 'the alt text'
		elseif key == 'tooltip' then
			return 'the tooltip text'
		elseif key == 'link' then
			return 'the link text'
		end
	end
	
	local obj = Padlock.new(protectionObj, blurbObj, cfg)

	self:assertEquals(20, obj.imageWidth)
	self:assertEquals('the tooltip text', obj.imageCaption)
	self:assertEquals('the alt text', obj._imageAlt)
	self:assertEquals('the link text', obj._imageLink)
end

function suite:testPadlockNewIndicators()
	local protectionObj = makeDefaultProtectionObject()
	protectionObj.action = 'move'
	protectionObj.level = 'sysop'
	local blurbObj = makeDefaultBlurbObject()

	local cfg = makeConfig{padlockIndicatorNames = {
		move = 'move-indicator',
		default = 'default-indicator'
	}}
	local obj = Padlock.new(protectionObj, blurbObj, cfg)
	self:assertEquals(obj._indicatorName, 'move-indicator')

	cfg.padlockIndicatorNames.move = nil
	obj = Padlock.new(protectionObj, blurbObj, cfg)
	self:assertEquals(obj._indicatorName, 'default-indicator')

	cfg.padlockIndicatorNames.default = nil
	obj = Padlock.new(protectionObj, blurbObj, cfg)
	self:assertEquals(obj._indicatorName, 'pp-default')
end

-- __tostring

function suite:testPadlockToString()
	local obj = makeDefaultPadlockObject()
	self:assertIsPadlock(tostring(obj))
end

--------------------------------------------------------------------------------
-- Export tests
--------------------------------------------------------------------------------

-- _main

function suite:test_mainError()
	local args = {expiry = 'foo', action = 'autoreview' }
	local cfg = makeConfig()
	local title
	local success, result = pcall(mProtectionBanner._main, args, cfg, title)
	self:assertFalse(success)
	self:assertEquals(
		'invalid expiry date: foo',
		result,
		false
	)
end

function suite:test_mainSmall1()
	local args = {small = 'yes'}
	local cfg = makeConfig()
	local title = makeDefaultTitleObject()
	self:assertIsPadlock(mProtectionBanner._main(args, cfg, title))
end

function suite:test_mainSmall2()
	local args = {small = 'Yes'}
	local cfg = makeConfig()
	local title = makeDefaultTitleObject()
	self:assertIsPadlock(mProtectionBanner._main(args, cfg, title))
end

function suite:test_mainSmall3()
	local args = {small = 'true'}
	local cfg = makeConfig()
	local title = makeDefaultTitleObject()
	self:assertIsPadlock(mProtectionBanner._main(args, cfg, title))
end

function suite:test_mainLarge1()
	local args = {}
	local cfg = makeConfig()
	local title = makeDefaultTitleObject()
	self:assertIsBanner(mProtectionBanner._main(args, cfg, title))
end

function suite:test_mainLarge2()
	local args = {small = 'no'}
	local cfg = makeConfig()
	local title = makeDefaultTitleObject()
	self:assertIsBanner(mProtectionBanner._main(args, cfg, title))
end

function suite:test_mainLarge3()
	local args = {small = 'No'}
	local cfg = makeConfig()
	local title = makeDefaultTitleObject()
	self:assertIsBanner(mProtectionBanner._main(args, cfg, title))
end

function suite:test_mainLarge4()
	local args = {small = 'false'}
	local cfg = makeConfig()
	local title = makeDefaultTitleObject()
	self:assertIsBanner(mProtectionBanner._main(args, cfg, title))
end

function suite:test_mainNoBanner()
	local args = {}
	local cfg = makeConfig()
	local title = makeTitleObject(d.page, 'edit', nil)
	self:assertNoBanner(mProtectionBanner._main(args, cfg, title), 'page unprotected')
end

function suite:test_mainCategories()
	local args = {}
	local cfg -- Use main config module
	local title = makeTitleObject(d.page, 'edit', nil)
	self:assertStringContains(
		'%[%[Category:.-%]%]',
		mProtectionBanner._main(args, cfg, title),
		false,
		'page unprotected'
	)
end

-- p.main

function suite:testMainHasOutput()
	local frame = mw.getCurrentFrame()
	local parent = frame:newChild{args = {}}
	local child = parent:newChild{args = {}}
	local cfg -- Use main config module
	self:assertStringContains('%S', mProtectionBanner.main(child, cfg), false)
end

function suite:testMainWrapper()
	local frame = mw.getCurrentFrame()
	local parent = frame:newChild{title = 'Template:Pp-example', args = {}}
	local child = parent:newChild{args = {}}
	local cfg = makeConfig{
		msg = {['tracking-category-incorrect'] = 'Incorrect'},
		wrappers = {['Template:Pp-example'] = {category = false}}
	}
	self:assertNotStringContains(
		'%[%[Category:.-%]%]',
		mProtectionBanner.main(child, cfg),
		false
	)
	self:assertStringContains(
		'%[%[Category:.-%]%]',
		mProtectionBanner.main(parent, cfg),
		false
	)
end

function suite:testMainWrapperOverride()
	local frame = mw.getCurrentFrame()
	local parent = frame:newChild{title = 'Template:Pp-example', args = {category = 'yes'}}
	local child = parent:newChild{args = {}}
	local cfg = makeConfig{
		msg = {['tracking-category-incorrect'] = 'Incorrect'},
		wrappers = {['Template:Pp-example'] = {category = false}}
	}
	self:assertStringContains(
		'%[%[Category:.-%]%]',
		mProtectionBanner.main(child, cfg),
		false
	)
end

function suite:testMainWrapperSandbox()
	local frame = mw.getCurrentFrame()
	local parent = frame:newChild{title = 'Template:Pp-example/sandbox', args = {}}
	local child = parent:newChild{args = {}}
	local cfg = makeConfig{
		msg = {['tracking-category-incorrect'] = 'Incorrect'},
		wrappers = {['Template:Pp-example'] = {category = false}}
	}
	self:assertNotStringContains(
		'%[%[Category:.-%]%]',
		mProtectionBanner.main(child, cfg),
		false
	)
	self:assertStringContains(
		'%[%[Category:.-%]%]',
		mProtectionBanner.main(parent, cfg),
		false
	)
end

function suite:testMainNoWrapper()
	local frame = mw.getCurrentFrame()
	local parent = frame:newChild{title = 'Template:Some template', args = {}}
	local child = parent:newChild{args = {}}
	local cfg = makeConfig{
		msg = {['tracking-category-incorrect'] = 'Incorrect'},
		wrappers = {['Template:Pp-example'] = {category = false}}
	}
	self:assertStringContains(
		'%[%[Category:.-%]%]',
		mProtectionBanner.main(child, cfg),
		false
	)
	self:assertStringContains(
		'%[%[Category:.-%]%]',
		mProtectionBanner.main(parent, cfg),
		false
	)
end

return suite