104 lines
5.5 KiB
JavaScript
104 lines
5.5 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
const initSqlJs = require('sql.js');
|
|
const migration = require('../../src/database/migrations/0013-create-trophy-local-snapshots');
|
|
const {
|
|
createSimulatedLocalTrophyProvider,
|
|
createSqliteLocalTrophyRepository,
|
|
createUnsupportedLocalTrophyProvider,
|
|
parseLocalTrophySnapshot,
|
|
} = require('../../src/services/local-trophy-inventory-service');
|
|
|
|
function sample(status = 'complete') {
|
|
return {
|
|
schemaVersion: 1,
|
|
provider: { id: 'simulated-local-reader', version: '1.0.0', firmware: '9.60', capabilities: ['profile-identity', 'list-title-sets', 'list-earned-trophies', 'earned-timestamps'] },
|
|
profile: { profileId: 'profile-12345678', localUserId: 'user-87654321', accountId: '1234567890123456' },
|
|
coverage: { status, completedScopes: status === 'inaccessible' ? [] : ['profile', 'NPWR12345_00'], failedScopes: status === 'complete' ? [] : ['NPWR54321_00'] },
|
|
observedAt: '2026-07-21T12:00:00.000Z',
|
|
titles: [{
|
|
npCommunicationId: 'NPWR12345_00', titleId: 'PPSA12345', platform: 'PS5', service: 'trophy2',
|
|
trophies: [
|
|
{ groupId: 'default', trophyId: 2, earned: false },
|
|
{ groupId: 'default', trophyId: 1, earned: true, earnedDateTime: '2026-01-02T03:04:05Z', definitionHash: 'a'.repeat(64) },
|
|
],
|
|
}],
|
|
};
|
|
}
|
|
|
|
async function setup() {
|
|
const SQL = await initSqlJs();
|
|
const db = new SQL.Database();
|
|
migration.up(db);
|
|
return { db, repository: createSqliteLocalTrophyRepository({ db }) };
|
|
}
|
|
|
|
test('complete local snapshot preserves exact IDs and alone authorizes absence conclusions', async t => {
|
|
const { db, repository } = await setup();
|
|
t.after(() => db.close());
|
|
const provider = createSimulatedLocalTrophyProvider({ repository, now: () => 1800000000000 });
|
|
const record = provider.fetchSnapshot(sample('complete'));
|
|
assert.equal(record.snapshot.profile.profileId, 'profile-12345678');
|
|
assert.equal(record.snapshot.titles[0].npCommunicationId, 'NPWR12345_00');
|
|
assert.deepEqual(record.snapshot.titles[0].trophies.map(item => item.trophyId), [1, 2]);
|
|
assert.equal(record.snapshot.coverage.canConcludeAbsence, true);
|
|
assert.equal(record.snapshot.provider.firmware, '9.60');
|
|
assert.deepEqual(record.snapshot.provider.capabilities, ['earned-timestamps', 'list-earned-trophies', 'list-title-sets', 'profile-identity']);
|
|
assert.match(record.rawSha256, /^[a-f0-9]{64}$/);
|
|
assert.throws(() => db.run('UPDATE trophy_local_snapshots SET firmware = ?', ['10.00']), /immutable/);
|
|
});
|
|
|
|
test('partial and inaccessible snapshots can never authorize trophy absence', async t => {
|
|
const { db, repository } = await setup();
|
|
t.after(() => db.close());
|
|
let now = 1800000000000;
|
|
const provider = createSimulatedLocalTrophyProvider({ repository, now: () => now++ });
|
|
const partial = provider.fetchSnapshot(sample('partial'));
|
|
const inaccessible = provider.fetchSnapshot(sample('inaccessible'));
|
|
assert.equal(partial.snapshot.coverage.canConcludeAbsence, false);
|
|
assert.equal(inaccessible.snapshot.coverage.canConcludeAbsence, false);
|
|
assert.deepEqual(partial.snapshot.coverage.failedScopes, ['NPWR54321_00']);
|
|
const invalid = sample('complete');
|
|
invalid.coverage.failedScopes = ['unreadable-title'];
|
|
assert.throws(() => parseLocalTrophySnapshot(invalid), error => error.code === 'LOCAL_TROPHY_SCHEMA');
|
|
});
|
|
|
|
test('unsupported live provider is explicit and exposes no mutation capability', () => {
|
|
const provider = createUnsupportedLocalTrophyProvider({ firmware: '10.00', reason: 'Pinned read-only adapter is unavailable for this firmware.' });
|
|
assert.deepEqual(Object.keys(provider).sort(), ['capabilities', 'fetchSnapshot', 'id', 'sanitizePublicModel', 'status']);
|
|
assert.deepEqual(provider.capabilities, []);
|
|
assert.equal(provider.status().available, false);
|
|
assert.equal(provider.status().readOnly, true);
|
|
assert.match(provider.status().reason, /unavailable/);
|
|
assert.throws(() => provider.fetchSnapshot(), error => error.code === 'LOCAL_TROPHY_UNSUPPORTED');
|
|
});
|
|
|
|
test('public local model masks profile identities and invalid evidence fails closed', async t => {
|
|
const { db, repository } = await setup();
|
|
t.after(() => db.close());
|
|
const provider = createSimulatedLocalTrophyProvider({ repository, now: () => 1800000000000 });
|
|
const publicModel = provider.sanitizePublicModel(provider.fetchSnapshot(sample()));
|
|
assert.equal(JSON.stringify(publicModel).includes('1234567890123456'), false);
|
|
assert.equal(JSON.stringify(publicModel).includes('profile-12345678'), false);
|
|
assert.equal(publicModel.coverage.canConcludeAbsence, true);
|
|
assert.equal(publicModel.readOnly, true);
|
|
|
|
const noTimestamp = sample();
|
|
delete noTimestamp.titles[0].trophies[1].earnedDateTime;
|
|
assert.throws(() => parseLocalTrophySnapshot(noTimestamp), error => error.code === 'LOCAL_TROPHY_SCHEMA');
|
|
const hostile = sample();
|
|
hostile.provider.capabilities.push('write-trophies');
|
|
assert.throws(() => parseLocalTrophySnapshot(hostile), error => error.code === 'LOCAL_TROPHY_SCHEMA');
|
|
});
|
|
|
|
test('schema and source contain only read-only local provider surfaces', () => {
|
|
const schema = require('../../schemas/trophy-local-inventory.schema.json');
|
|
assert.equal(schema.additionalProperties, false);
|
|
const source = fs.readFileSync(path.resolve(__dirname, '../../src/services/local-trophy-inventory-service.js'), 'utf8');
|
|
assert.doesNotMatch(source, /(?:unlock|reconstruct|applyTrophy|writeTrophy|deleteTrophy)/i);
|
|
assert.match(source, /canConcludeAbsence:\s*coverageStatus === 'complete'/);
|
|
}); |