-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added test implementation and helpers. Added initial workflow. Lock
- Loading branch information
Showing
8 changed files
with
938 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
name: test | ||
|
||
on: | ||
push: | ||
branches: [main] | ||
pull_request: | ||
branches: [main] | ||
|
||
jobs: | ||
test: | ||
name: Run tests | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v3 | ||
|
||
- name: Setup Deno | ||
uses: ./ | ||
with: | ||
deno-version: 1.39.4 | ||
|
||
- name: Test | ||
run: deno test --allow-env --allow-read --trace-ops |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
import { assertArrayIncludes, assertEquals } from './internal/deps.ts'; | ||
import { stubFetch, stubReadTextFile } from './internal/stubs.ts'; | ||
import { createFixedTimepoint, moveTimeToPosition } from './internal/time.ts'; | ||
import createSlackActionsApi from './slack.ts'; | ||
|
||
Deno.test('Simple schedule test', async () => { | ||
const helpers = await basicTestSetup(` | ||
[first-status] | ||
start = 16:00:00 | ||
end = 17:00:00 | ||
icon = ":test:" | ||
message = [ | ||
"This is my test status message", | ||
"This is another possible status message" | ||
] | ||
days = ["Mon", "Tue", "Wed", "Thu", "Fri"] | ||
doNotDisturb = true | ||
[second-status] | ||
start = 18:00:00 | ||
end = 20:00:00 | ||
icon = ":test2:" | ||
message = [ | ||
"EEEEEE", | ||
"AAAAAAH" | ||
] | ||
days = ["Tue", "Wed", "Thu", "Fri"] | ||
`); | ||
|
||
const { time, runtime } = helpers; | ||
|
||
try { | ||
// set a specific time that we know | ||
moveTimeToPosition(time, '16:03'); | ||
|
||
// Queue the requests we expect to intercept - note that missing requests will fail the test | ||
const userInfoRequest1 = helpers.slackApi.getProfileRequest(); | ||
const dndInfoRequest1 = helpers.slackApi.getDndInfoRequest(); | ||
const userSetRequest1 = helpers.slackApi.updateProfileRequest(); | ||
const dndSetRequest1 = helpers.slackApi.setDndSnoozeRequest(); | ||
|
||
// First cycle iteration | ||
await Promise.all([ | ||
runtime.executeTask(), | ||
userInfoRequest1, | ||
dndInfoRequest1, | ||
userSetRequest1, | ||
dndSetRequest1, | ||
]); | ||
|
||
// Verify that the expected values were set | ||
helpers.slackApi.assert((state) => { | ||
assertEquals(state.statusEmoji, ':test:'); | ||
assertEquals(state.statusExpiration, 820533600); | ||
assertEquals(state.dndDurationMinutes, 56); | ||
assertArrayIncludes([ | ||
'This is my test status message', | ||
'This is another possible status message', | ||
], [state.statusText]); | ||
}); | ||
|
||
// move time to empty allocation (status should unset) | ||
moveTimeToPosition(helpers.time, '17:30'); | ||
|
||
// Again, queue the requests we expect to intercept | ||
const userInfoRequest2 = helpers.slackApi.getProfileRequest(); | ||
const dndInfoRequest2 = helpers.slackApi.getDndInfoRequest(); | ||
const userSetRequest2 = helpers.slackApi.updateProfileRequest(); | ||
const dndEndRequest2 = helpers.slackApi.endDndSnoozeRequest(); | ||
|
||
// Second iteration (should empty status, and end DnD) | ||
await Promise.all([ | ||
runtime.executeTask(), | ||
userInfoRequest2, | ||
dndInfoRequest2, | ||
userSetRequest2, | ||
dndEndRequest2, | ||
]); | ||
|
||
// Verify that mocked Slack api has unset state | ||
helpers.slackApi.assert((state) => { | ||
assertEquals(state.statusEmoji, null); | ||
assertEquals(state.statusExpiration, null); | ||
assertEquals(state.dndDurationMinutes, 0); | ||
assertEquals(state.statusText, ''); | ||
}); | ||
|
||
// move time to next allocation (no overlap because it should be monday) | ||
moveTimeToPosition(helpers.time, '18:10'); | ||
|
||
// Queue the requests we expect to intercept | ||
const userInfoRequest3 = helpers.slackApi.getProfileRequest(); | ||
const dndInfoRequest3 = helpers.slackApi.getDndInfoRequest(); | ||
|
||
// Perform work cycle | ||
await Promise.all([ | ||
runtime.executeTask(), | ||
userInfoRequest3, | ||
dndInfoRequest3, | ||
]); | ||
|
||
// Verify that mocked Slack api has unset state | ||
helpers.slackApi.assert((state) => { | ||
assertEquals(state.statusEmoji, null); | ||
assertEquals(state.statusExpiration, null); | ||
assertEquals(state.dndDurationMinutes, 0); | ||
assertEquals(state.statusText, ''); | ||
}); | ||
|
||
// move time to 19:00 hours on the following day (should be Tuesday) | ||
moveTimeToPosition(helpers.time, '19:00', 1); | ||
|
||
const userInfoRequest4 = helpers.slackApi.getProfileRequest(); | ||
const dndInfoRequest4 = helpers.slackApi.getDndInfoRequest(); | ||
const userSetRequest4 = helpers.slackApi.updateProfileRequest(); | ||
|
||
await Promise.all([ | ||
runtime.executeTask(), | ||
userInfoRequest4, | ||
dndInfoRequest4, | ||
userSetRequest4, | ||
]); | ||
|
||
helpers.slackApi.assert((state) => { | ||
assertEquals(state.statusEmoji, ':test2:'); | ||
assertEquals(state.statusExpiration, 820630800); | ||
assertEquals(state.dndDurationMinutes, 0); | ||
assertArrayIncludes([ | ||
'EEEEEE', | ||
'AAAAAAH', | ||
], [state.statusText]); | ||
}); | ||
|
||
// await duration(1000); | ||
} catch (error) { | ||
throw error; | ||
} finally { | ||
console.log('cleanup'); | ||
helpers.cleanup(); | ||
} | ||
}); | ||
|
||
async function basicTestSetup( | ||
scheduleTomlFile: string, | ||
) { | ||
// Setup stubs (clean after test) | ||
const time = createFixedTimepoint(2, 1, 1996); | ||
const fetchStub = stubFetch(); | ||
const readFileStub = stubReadTextFile(); | ||
|
||
// Create fake secret and stub | ||
readFileStub.set(`/run/secrets/slack_status_scheduler_user_token`, 'slack_status_scheduler_user_token'); | ||
// Create toml schedule representation and stub | ||
readFileStub.set('/schedule.toml', scheduleTomlFile); | ||
|
||
// create mock Slack api | ||
const slackApi = createSlackActionsApi(fetchStub); | ||
|
||
// load runtime as import module | ||
const runtimeModule = await import('../core/runtime.ts'); | ||
const runtime = runtimeModule.createRuntimeScope({ crashOnException: true }); | ||
|
||
return { | ||
runtime, | ||
time, | ||
fetchStub, | ||
slackApi, | ||
readFileStub, | ||
cleanup() { | ||
// runtime?.stop(); | ||
time.restore(); | ||
fetchStub.cleanup(); | ||
readFileStub.cleanup(); | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { setTimeout } from './time.ts'; | ||
|
||
/** | ||
* Creates a simple promise | ||
* @param data | ||
* @returns | ||
*/ | ||
export function createSimplePromise<ResolveData>( | ||
data: ResolveData, | ||
): Promise<ResolveData> { | ||
return new Promise((resolve) => resolve(data)); | ||
} | ||
|
||
/** | ||
* Creates a shallow clone (useful in assertions) using JSON internals | ||
* @param jsonFriendlyData The data to clone | ||
* @returns The replicated object | ||
*/ | ||
export function shallowClone<DataType = unknown>(jsonFriendlyData: DataType): DataType { | ||
return JSON.parse(JSON.stringify(jsonFriendlyData)); | ||
} | ||
|
||
type DataMapKeyType = string | RegExp | undefined; | ||
|
||
/** | ||
* Checks a string against a DataMapKey (string or Regexp) | ||
* @param value The value to test | ||
* @param testAgainst The test conditions (string as direct, regexp as well - regexp) | ||
* @returns The test result | ||
*/ | ||
export function stringMatchesKey(value: string, testAgainst: DataMapKeyType): boolean { | ||
return (testAgainst instanceof RegExp && testAgainst.test(value)) || | ||
(value === testAgainst); | ||
} | ||
|
||
/** | ||
* Iterate through the data map and check if any keys are matches (processes regular expressions) | ||
* This function, for the most part, only accounts for functions that are string based (no fancy post processing) | ||
* @param keyToCheck | ||
* @returns | ||
*/ | ||
export function getDataFromKey<MapDataType>( | ||
dataMap: Map<DataMapKeyType, MapDataType>, | ||
keyToCheck: string, | ||
): MapDataType | null { | ||
for (const [key, value] of dataMap) { | ||
if (stringMatchesKey(keyToCheck, key)) { | ||
return value; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* Parses JSON if it can, otherwise returns null. Useful for fetch interceptors. | ||
* @param probablyJSONValue | ||
* @returns | ||
*/ | ||
export function jsonParseOrNull(probablyJSONValue: string) { | ||
try { | ||
return JSON.parse(probablyJSONValue); | ||
} catch (_e) { | ||
// noop | ||
} | ||
return null; | ||
} | ||
|
||
/** | ||
* Wait a minimum amount of time on the event loop | ||
* @param timeInMs | ||
* @returns | ||
*/ | ||
export function duration(timeInMs: number) { | ||
return new Promise((resolve) => setTimeout(() => resolve(null), timeInMs)); | ||
} | ||
|
||
/** | ||
* @unused | ||
* Creates a handler for capturing async errors on the event loop that suddenly | ||
* stop propagating. At the end of tests, this gets executed. | ||
* @returns | ||
*/ | ||
export function _captureEventLoopErrors() { | ||
const eventLoopErrors: Error[] = []; | ||
|
||
function onUnhandledRejection(e: PromiseRejectionEvent) { | ||
// Track the error | ||
eventLoopErrors.push(e.reason as Error); | ||
// Don't allow other code to track this exception | ||
e.preventDefault(); | ||
e.stopImmediatePropagation(); | ||
} | ||
|
||
globalThis.addEventListener('unhandledrejection', onUnhandledRejection); | ||
|
||
return { | ||
check() { | ||
if (!eventLoopErrors.length) return; | ||
|
||
console.error(`(${eventLoopErrors.length}) rejections detected`); | ||
|
||
eventLoopErrors.forEach((error, errorIndex) => { | ||
console.error(`(${errorIndex})`); | ||
console.error(error); | ||
}); | ||
|
||
throw new Error(`(${eventLoopErrors.length}) rejections detected`); | ||
}, | ||
cleanup() { | ||
// remove the handler | ||
globalThis.removeEventListener('unhandledrejection', onUnhandledRejection); | ||
// nudge gc | ||
eventLoopErrors.length = 0; | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export { FakeTime } from 'https://deno.land/[email protected]/testing/time.ts'; | ||
export { _internals as FakeTimeInternals } from 'https://deno.land/[email protected]/testing/_time.ts'; | ||
|
||
export { assertArrayIncludes, assertEquals } from 'https://deno.land/[email protected]/assert/mod.ts'; |
Oops, something went wrong.