Skip to content

Commit 06ec159

Browse files
taylortomclaude
andcommitted
New: Add unit tests (fixes #426)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b02f286 commit 06ec159

File tree

5 files changed

+485
-1
lines changed

5 files changed

+485
-1
lines changed

.github/workflows/tests.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Tests
2+
on: push
3+
jobs:
4+
default:
5+
runs-on: ubuntu-latest
6+
permissions:
7+
contents: read
8+
steps:
9+
- uses: actions/checkout@v4
10+
- uses: actions/setup-node@v4
11+
with:
12+
node-version: 'lts/*'
13+
cache: 'npm'
14+
- run: npm ci
15+
- run: npm test

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"type": "module",
88
"main": "index.js",
99
"scripts": {
10-
"postinstall": "node npm_hooks/postinstall.js"
10+
"postinstall": "node npm_hooks/postinstall.js",
11+
"test": "node --test 'tests/**/*.spec.js'"
1112
},
1213
"repository": "github:adapt-security/adapt-authoring-ui",
1314
"dependencies": {

tests/CacheManager.spec.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { describe, it, beforeEach } from 'node:test'
2+
import assert from 'node:assert/strict'
3+
import crypto from 'crypto'
4+
import os from 'os'
5+
import path from 'path'
6+
import fs from 'fs-extra'
7+
import CacheManager from '../lib/CacheManager.js'
8+
9+
describe('CacheManager', () => {
10+
describe('constructor', () => {
11+
it('should set maxAge to the provided value', () => {
12+
const cm = new CacheManager({ maxAge: 1000, logger: { log () {} } })
13+
assert.equal(cm.maxAge, 1000)
14+
})
15+
16+
it('should use ONE_WEEK as default maxAge', () => {
17+
const ONE_WEEK = 7 * 24 * 60 * 60 * 1000
18+
const cm = new CacheManager({ logger: { log () {} } })
19+
assert.equal(cm.maxAge, ONE_WEEK)
20+
})
21+
22+
it('should set the provided logger', () => {
23+
const logger = { log () {} }
24+
const cm = new CacheManager({ logger })
25+
assert.equal(cm.logger, logger)
26+
})
27+
28+
it('should use a default logger when none is provided', () => {
29+
const cm = new CacheManager()
30+
assert.equal(typeof cm.logger.log, 'function')
31+
})
32+
33+
it('should use provided tempDir', () => {
34+
const tempDir = path.join(os.tmpdir(), 'adapt-authoring-test-custom')
35+
const cm = new CacheManager({ tempDir, logger: { log () {} } })
36+
assert.equal(cm.tempDir, tempDir)
37+
// Cleanup
38+
try { fs.rmdirSync(tempDir) } catch (e) {}
39+
})
40+
41+
it('should use default tempDir when none is provided', () => {
42+
const cm = new CacheManager({ logger: { log () {} } })
43+
assert.equal(cm.tempDir, path.join(os.tmpdir(), 'adapt-authoring'))
44+
})
45+
46+
it('should ensure tempDir directory exists', () => {
47+
const cm = new CacheManager({ logger: { log () {} } })
48+
assert.ok(fs.existsSync(cm.tempDir))
49+
})
50+
})
51+
52+
describe('static hash', () => {
53+
it('should return a SHA1 hex digest of the input', () => {
54+
const input = '/some/path/to/file'
55+
const expected = crypto
56+
.createHash('sha1')
57+
.update(input, 'utf8')
58+
.digest('hex')
59+
assert.equal(CacheManager.hash(input), expected)
60+
})
61+
62+
it('should return different hashes for different inputs', () => {
63+
const hash1 = CacheManager.hash('/path/one')
64+
const hash2 = CacheManager.hash('/path/two')
65+
assert.notEqual(hash1, hash2)
66+
})
67+
68+
it('should return the same hash for the same input', () => {
69+
const hash1 = CacheManager.hash('/same/path')
70+
const hash2 = CacheManager.hash('/same/path')
71+
assert.equal(hash1, hash2)
72+
})
73+
74+
it('should return a 40-character hex string', () => {
75+
const hash = CacheManager.hash('test')
76+
assert.match(hash, /^[0-9a-f]{40}$/)
77+
})
78+
})
79+
80+
describe('cachePath', () => {
81+
it('should return a .cache file path under tempDir', () => {
82+
const cm = new CacheManager({ logger: { log () {} } })
83+
const result = cm.cachePath('/base', '/output')
84+
assert.ok(result.startsWith(cm.tempDir))
85+
assert.ok(result.endsWith('.cache'))
86+
})
87+
88+
it('should incorporate both basePath and outputFilePath into the hash', () => {
89+
const cm = new CacheManager({ logger: { log () {} } })
90+
const result1 = cm.cachePath('/base1', '/output')
91+
const result2 = cm.cachePath('/base2', '/output')
92+
assert.notEqual(result1, result2)
93+
})
94+
95+
it('should use process.cwd() as default outputFilePath', () => {
96+
const cm = new CacheManager({ logger: { log () {} } })
97+
const expectedHash = CacheManager.hash(path.join('/base', process.cwd()))
98+
const expected = path.join(cm.tempDir, `${expectedHash}.cache`)
99+
assert.equal(cm.cachePath('/base'), expected)
100+
})
101+
102+
it('should produce a deterministic result for same inputs', () => {
103+
const cm = new CacheManager({ logger: { log () {} } })
104+
const result1 = cm.cachePath('/base', '/output')
105+
const result2 = cm.cachePath('/base', '/output')
106+
assert.equal(result1, result2)
107+
})
108+
})
109+
110+
describe('checkFilePath', () => {
111+
it('should return a last.touch file path under tempDir', () => {
112+
const cm = new CacheManager({ logger: { log () {} } })
113+
assert.equal(cm.checkFilePath, path.join(cm.tempDir, 'last.touch'))
114+
})
115+
})
116+
117+
describe('isCleaningTime', () => {
118+
let cm
119+
120+
beforeEach(() => {
121+
cm = new CacheManager({ maxAge: 1000, logger: { log () {} } })
122+
})
123+
124+
it('should return true when checkFile does not exist', async () => {
125+
const checkPath = cm.checkFilePath
126+
try { fs.unlinkSync(checkPath) } catch (e) {}
127+
const result = await cm.isCleaningTime()
128+
assert.equal(result, true)
129+
})
130+
131+
it('should return false when checkFile was just created', async () => {
132+
const checkPath = cm.checkFilePath
133+
fs.writeFileSync(checkPath, String(Date.now()))
134+
const result = await cm.isCleaningTime()
135+
assert.equal(result, false)
136+
})
137+
})
138+
139+
describe('clean', () => {
140+
let cm
141+
142+
beforeEach(() => {
143+
// Use a large maxAge so the checkFile is not treated as expired
144+
cm = new CacheManager({ maxAge: 7 * 24 * 60 * 60 * 1000, logger: { log () {} } })
145+
fs.ensureDirSync(cm.tempDir)
146+
// Remove checkFile to ensure cleaning happens
147+
try { fs.unlinkSync(cm.checkFilePath) } catch (e) {}
148+
})
149+
150+
it('should create checkFile after cleaning', async () => {
151+
await cm.clean()
152+
assert.ok(fs.existsSync(cm.checkFilePath))
153+
})
154+
155+
it('should not clean when it is not cleaning time', async () => {
156+
// First write the checkFile so it appears fresh
157+
fs.writeFileSync(cm.checkFilePath, String(Date.now()))
158+
// Set a long maxAge so checkInterval is large
159+
cm.maxAge = 7 * 24 * 60 * 60 * 1000
160+
// Create a test file
161+
const testFile = path.join(cm.tempDir, 'test-no-clean.cache')
162+
fs.writeFileSync(testFile, 'data')
163+
await cm.clean()
164+
// File should still exist since it is not cleaning time
165+
assert.ok(fs.existsSync(testFile))
166+
// Cleanup
167+
try { fs.unlinkSync(testFile) } catch (e) {}
168+
})
169+
})
170+
})

0 commit comments

Comments
 (0)