Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/models/apikey.model.js
Original file line number Diff line number Diff line change
@@ -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', {
Expand Down
2 changes: 2 additions & 0 deletions app/models/apimaster.model.js
Original file line number Diff line number Diff line change
@@ -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', {
Expand Down
2 changes: 2 additions & 0 deletions app/models/customer.model.js
Original file line number Diff line number Diff line change
@@ -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', {
Expand Down
64 changes: 64 additions & 0 deletions tests/unit/spdx-headers.test.js
Original file line number Diff line number Diff line change
@@ -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');
},
);
});