diff --git a/app/models/apikey.model.js b/app/models/apikey.model.js index 6111239..bcb2c46 100644 --- a/app/models/apikey.model.js +++ b/app/models/apikey.model.js @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Aaron K. Clark 'use strict'; module.exports = (sequelize, Sequelize) => { const ApiKey = sequelize.define('ApiKey', { diff --git a/app/models/apimaster.model.js b/app/models/apimaster.model.js index 9db8f37..d6ec568 100644 --- a/app/models/apimaster.model.js +++ b/app/models/apimaster.model.js @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Aaron K. Clark 'use strict'; module.exports = (sequelize, Sequelize) => { const ApiMaster = sequelize.define('ApiMaster', { diff --git a/app/models/customer.model.js b/app/models/customer.model.js index 38332dc..29d2a8e 100644 --- a/app/models/customer.model.js +++ b/app/models/customer.model.js @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Aaron K. Clark 'use strict'; module.exports = (sequelize, Sequelize) => { const Customer = sequelize.define('Customer', { diff --git a/tests/unit/spdx-headers.test.js b/tests/unit/spdx-headers.test.js new file mode 100644 index 0000000..308a2c3 --- /dev/null +++ b/tests/unit/spdx-headers.test.js @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Aaron K. Clark +// +// Source-level regression pin: every JS file in app/, server.js, and +// tests/ carries the Apache-2.0 SPDX header on its first line. The +// project convention surfaces the license at the top of every file +// rather than relying solely on LICENSE at the repo root — easier +// for downstream forks and Apache-2.0 §4(c) re-distributors who +// receive a single file out of context. +// +// 3/18 model files (apikey.model.js, apimaster.model.js, +// customer.model.js) shipped without the header for several months +// before this test landed. Pin the policy here so future copy-paste +// of a header-less template fails CI immediately. + +import { describe, test, expect } from 'vitest'; +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { resolve, join } from 'node:path'; + +const REPO_ROOT = resolve(__dirname, '../..'); + +// Files / directories we deliberately don't scan: +// - tests/integration/ test files: already covered via the +// same pattern but skipped here to keep the per-file count +// focused on production code's contract. +const SCAN_ROOTS = [ + 'app', + // server.js gets covered via the single-file fallback below. +]; + +function walk(absDir, acc = []) { + for (const entry of readdirSync(absDir)) { + const abs = join(absDir, entry); + const stat = statSync(abs); + if (stat.isDirectory()) { + walk(abs, acc); + } else if (entry.endsWith('.js')) { + acc.push(abs); + } + } + return acc; +} + +const files = []; +for (const root of SCAN_ROOTS) { + files.push(...walk(resolve(REPO_ROOT, root))); +} +files.push(resolve(REPO_ROOT, 'server.js')); + +describe('SPDX-License-Identifier header coverage', () => { + test('discovers a non-trivial number of source files', () => { + // Floor-check so a misconfigured walk doesn't vacuously pass. + expect(files.length).toBeGreaterThan(40); + }); + + test.each(files.map((f) => [f.replace(REPO_ROOT + '/', '')]))( + '%s starts with SPDX-License-Identifier: Apache-2.0', + (relPath) => { + const abs = resolve(REPO_ROOT, relPath); + const first = readFileSync(abs, 'utf8').split('\n', 1)[0]; + expect(first).toBe('// SPDX-License-Identifier: Apache-2.0'); + }, + ); +});