Add unit tests for models and services

- Implemented unit tests for the Package model, covering methods such as generateId, findById, findByName, and version management.
- Created unit tests for the Repository model, including repository creation, name validation, and retrieval methods.
- Added tests for the Session model, focusing on session creation, validation, and invalidation.
- Developed unit tests for the User model, ensuring user creation, password hashing, and retrieval methods function correctly.
- Implemented AuthService tests, validating login, token refresh, and session management.
- Added TokenService tests, covering token creation, validation, and revocation processes.
This commit is contained in:
2025-11-28 15:27:04 +00:00
parent 61324ba195
commit 44e92d48f2
50 changed files with 4403 additions and 108 deletions

View File

@@ -0,0 +1,232 @@
/**
* ApiToken model unit tests
*/
import { assertEquals, assertExists } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { setupTestDb, teardownTestDb, cleanupTestDb, createTestUser } from '../../helpers/index.ts';
import { ApiToken } from '../../../ts/models/apitoken.ts';
describe('ApiToken Model', () => {
let testUserId: string;
beforeAll(async () => {
await setupTestDb();
});
afterAll(async () => {
await teardownTestDb();
});
beforeEach(async () => {
await cleanupTestDb();
const { user } = await createTestUser();
testUserId = user.id;
});
async function createToken(overrides: Partial<ApiToken> = {}): Promise<ApiToken> {
const token = new ApiToken();
token.id = await ApiToken.getNewId();
token.userId = overrides.userId || testUserId;
token.name = overrides.name || 'test-token';
token.tokenHash = overrides.tokenHash || `hash-${crypto.randomUUID()}`;
token.tokenPrefix = overrides.tokenPrefix || 'srg_test';
token.protocols = overrides.protocols || ['npm', 'oci'];
token.scopes = overrides.scopes || [{ protocol: '*', actions: ['read', 'write'] }];
token.createdAt = new Date();
if (overrides.expiresAt) token.expiresAt = overrides.expiresAt;
if (overrides.isRevoked) token.isRevoked = overrides.isRevoked;
if (overrides.organizationId) token.organizationId = overrides.organizationId;
await token.save();
return token;
}
describe('findByHash', () => {
it('should find token by hash', async () => {
const created = await createToken({ tokenHash: 'unique-hash-123' });
const found = await ApiToken.findByHash('unique-hash-123');
assertExists(found);
assertEquals(found.id, created.id);
});
it('should not find revoked tokens', async () => {
await createToken({
tokenHash: 'revoked-hash',
isRevoked: true,
});
const found = await ApiToken.findByHash('revoked-hash');
assertEquals(found, null);
});
});
describe('getUserTokens', () => {
it('should return all user tokens', async () => {
await createToken({ name: 'token1' });
await createToken({ name: 'token2' });
const tokens = await ApiToken.getUserTokens(testUserId);
assertEquals(tokens.length, 2);
});
it('should not return revoked tokens', async () => {
await createToken({ name: 'active' });
await createToken({ name: 'revoked', isRevoked: true });
const tokens = await ApiToken.getUserTokens(testUserId);
assertEquals(tokens.length, 1);
assertEquals(tokens[0].name, 'active');
});
});
describe('getOrgTokens', () => {
it('should return organization tokens', async () => {
const orgId = 'org-123';
await createToken({ name: 'org-token', organizationId: orgId });
await createToken({ name: 'personal-token' }); // No org
const tokens = await ApiToken.getOrgTokens(orgId);
assertEquals(tokens.length, 1);
assertEquals(tokens[0].name, 'org-token');
});
});
describe('isValid', () => {
it('should return true for valid token', async () => {
const token = await createToken();
assertEquals(token.isValid(), true);
});
it('should return false for revoked token', async () => {
const token = await createToken({ isRevoked: true });
assertEquals(token.isValid(), false);
});
it('should return false for expired token', async () => {
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - 1);
const token = await createToken({ expiresAt: pastDate });
assertEquals(token.isValid(), false);
});
it('should return true for non-expired token', async () => {
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 30);
const token = await createToken({ expiresAt: futureDate });
assertEquals(token.isValid(), true);
});
});
describe('recordUsage', () => {
it('should update usage stats', async () => {
const token = await createToken();
await token.recordUsage('192.168.1.1');
assertExists(token.lastUsedAt);
assertEquals(token.lastUsedIp, '192.168.1.1');
assertEquals(token.usageCount, 1);
});
it('should increment usage count', async () => {
const token = await createToken();
await token.recordUsage();
await token.recordUsage();
await token.recordUsage();
assertEquals(token.usageCount, 3);
});
});
describe('revoke', () => {
it('should revoke token with reason', async () => {
const token = await createToken();
await token.revoke('Security concern');
assertEquals(token.isRevoked, true);
assertExists(token.revokedAt);
assertEquals(token.revokedReason, 'Security concern');
});
it('should revoke token without reason', async () => {
const token = await createToken();
await token.revoke();
assertEquals(token.isRevoked, true);
assertExists(token.revokedAt);
assertEquals(token.revokedReason, undefined);
});
});
describe('hasProtocol', () => {
it('should return true for allowed protocol', async () => {
const token = await createToken({ protocols: ['npm', 'oci'] });
assertEquals(token.hasProtocol('npm'), true);
assertEquals(token.hasProtocol('oci'), true);
});
it('should return false for disallowed protocol', async () => {
const token = await createToken({ protocols: ['npm'] });
assertEquals(token.hasProtocol('maven'), false);
});
});
describe('hasScope', () => {
it('should allow wildcard protocol scope', async () => {
const token = await createToken({
scopes: [{ protocol: '*', actions: ['read', 'write'] }],
});
assertEquals(token.hasScope('npm'), true);
assertEquals(token.hasScope('oci'), true);
assertEquals(token.hasScope('maven'), true);
});
it('should restrict by specific protocol', async () => {
const token = await createToken({
scopes: [{ protocol: 'npm', actions: ['read'] }],
});
assertEquals(token.hasScope('npm'), true);
assertEquals(token.hasScope('oci'), false);
});
it('should restrict by organization', async () => {
const token = await createToken({
scopes: [{ protocol: '*', organizationId: 'org-123', actions: ['read'] }],
});
assertEquals(token.hasScope('npm', 'org-123'), true);
assertEquals(token.hasScope('npm', 'org-456'), false);
});
it('should check action permissions', async () => {
const token = await createToken({
scopes: [{ protocol: '*', actions: ['read'] }],
});
assertEquals(token.hasScope('npm', undefined, undefined, 'read'), true);
assertEquals(token.hasScope('npm', undefined, undefined, 'write'), false);
});
it('should allow wildcard action', async () => {
const token = await createToken({
scopes: [{ protocol: '*', actions: ['*'] }],
});
assertEquals(token.hasScope('npm', undefined, undefined, 'read'), true);
assertEquals(token.hasScope('npm', undefined, undefined, 'write'), true);
assertEquals(token.hasScope('npm', undefined, undefined, 'delete'), true);
});
});
});

View File

@@ -0,0 +1,220 @@
/**
* Organization model unit tests
*/
import { assertEquals, assertExists, assertRejects } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { setupTestDb, teardownTestDb, cleanupTestDb, createTestUser } from '../../helpers/index.ts';
import { Organization } from '../../../ts/models/organization.ts';
describe('Organization Model', () => {
let testUserId: string;
beforeAll(async () => {
await setupTestDb();
});
afterAll(async () => {
await teardownTestDb();
});
beforeEach(async () => {
await cleanupTestDb();
const { user } = await createTestUser();
testUserId = user.id;
});
describe('createOrganization', () => {
it('should create an organization with valid data', async () => {
const org = await Organization.createOrganization({
name: 'test-org',
displayName: 'Test Organization',
description: 'A test organization',
createdById: testUserId,
});
assertExists(org.id);
assertEquals(org.name, 'test-org');
assertEquals(org.displayName, 'Test Organization');
assertEquals(org.description, 'A test organization');
assertEquals(org.createdById, testUserId);
assertEquals(org.isPublic, false);
assertEquals(org.memberCount, 0);
assertEquals(org.plan, 'free');
});
it('should allow dots in org name (domain-like)', async () => {
const org = await Organization.createOrganization({
name: 'push.rocks',
displayName: 'Push Rocks',
createdById: testUserId,
});
assertEquals(org.name, 'push.rocks');
});
it('should allow hyphens in org name', async () => {
const org = await Organization.createOrganization({
name: 'my-awesome-org',
displayName: 'My Awesome Org',
createdById: testUserId,
});
assertEquals(org.name, 'my-awesome-org');
});
it('should reject uppercase names (must be lowercase)', async () => {
await assertRejects(
async () => {
await Organization.createOrganization({
name: 'UPPERCASE',
displayName: 'Uppercase Org',
createdById: testUserId,
});
},
Error,
'lowercase alphanumeric'
);
});
it('should reject invalid names starting with dot', async () => {
await assertRejects(
async () => {
await Organization.createOrganization({
name: '.invalid',
displayName: 'Invalid',
createdById: testUserId,
});
},
Error,
'lowercase alphanumeric'
);
});
it('should reject invalid names ending with dot', async () => {
await assertRejects(
async () => {
await Organization.createOrganization({
name: 'invalid.',
displayName: 'Invalid',
createdById: testUserId,
});
},
Error,
'lowercase alphanumeric'
);
});
it('should reject names with special characters', async () => {
await assertRejects(
async () => {
await Organization.createOrganization({
name: 'invalid@org',
displayName: 'Invalid',
createdById: testUserId,
});
},
Error,
'lowercase alphanumeric'
);
});
it('should set default settings', async () => {
const org = await Organization.createOrganization({
name: 'defaults',
displayName: 'Defaults Test',
createdById: testUserId,
});
assertEquals(org.settings.requireMfa, false);
assertEquals(org.settings.allowPublicRepositories, true);
assertEquals(org.settings.defaultRepositoryVisibility, 'private');
assertEquals(org.settings.allowedProtocols.length, 7);
});
});
describe('findById', () => {
it('should find organization by ID', async () => {
const created = await Organization.createOrganization({
name: 'findable',
displayName: 'Findable Org',
createdById: testUserId,
});
const found = await Organization.findById(created.id);
assertExists(found);
assertEquals(found.id, created.id);
});
it('should return null for non-existent ID', async () => {
const found = await Organization.findById('non-existent-id');
assertEquals(found, null);
});
});
describe('findByName', () => {
it('should find organization by name (case-insensitive)', async () => {
await Organization.createOrganization({
name: 'byname',
displayName: 'By Name',
createdById: testUserId,
});
const found = await Organization.findByName('BYNAME');
assertExists(found);
assertEquals(found.name, 'byname');
});
});
describe('storage quota', () => {
it('should have default 5GB quota', async () => {
const org = await Organization.createOrganization({
name: 'quota-test',
displayName: 'Quota Test',
createdById: testUserId,
});
assertEquals(org.storageQuotaBytes, 5 * 1024 * 1024 * 1024);
assertEquals(org.usedStorageBytes, 0);
});
it('should check available storage', async () => {
const org = await Organization.createOrganization({
name: 'storage-check',
displayName: 'Storage Check',
createdById: testUserId,
});
assertEquals(org.hasStorageAvailable(1024), true);
assertEquals(org.hasStorageAvailable(6 * 1024 * 1024 * 1024), false);
});
it('should allow unlimited storage with -1 quota', async () => {
const org = await Organization.createOrganization({
name: 'unlimited',
displayName: 'Unlimited',
createdById: testUserId,
});
org.storageQuotaBytes = -1;
assertEquals(org.hasStorageAvailable(1000 * 1024 * 1024 * 1024), true);
});
it('should update storage usage', async () => {
const org = await Organization.createOrganization({
name: 'usage-test',
displayName: 'Usage Test',
createdById: testUserId,
});
await org.updateStorageUsage(1000);
assertEquals(org.usedStorageBytes, 1000);
await org.updateStorageUsage(500);
assertEquals(org.usedStorageBytes, 1500);
await org.updateStorageUsage(-2000);
assertEquals(org.usedStorageBytes, 0); // Should not go negative
});
});
});

View File

@@ -0,0 +1,240 @@
/**
* Package model unit tests
*/
import { assertEquals, assertExists } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import {
setupTestDb,
teardownTestDb,
cleanupTestDb,
createTestUser,
createOrgWithOwner,
createTestRepository,
} from '../../helpers/index.ts';
import { Package } from '../../../ts/models/package.ts';
import type { IPackageVersion } from '../../../ts/interfaces/package.interfaces.ts';
describe('Package Model', () => {
let testUserId: string;
let testOrgId: string;
let testRepoId: string;
beforeAll(async () => {
await setupTestDb();
});
afterAll(async () => {
await teardownTestDb();
});
beforeEach(async () => {
await cleanupTestDb();
const { user } = await createTestUser();
testUserId = user.id;
const { organization } = await createOrgWithOwner(testUserId);
testOrgId = organization.id;
const repo = await createTestRepository({
organizationId: testOrgId,
createdById: testUserId,
protocol: 'npm',
});
testRepoId = repo.id;
});
function createVersion(version: string): IPackageVersion {
return {
version,
publishedAt: new Date(),
publishedBy: testUserId,
size: 1024,
checksum: `sha256-${crypto.randomUUID()}`,
checksumAlgorithm: 'sha256',
downloads: 0,
metadata: {},
};
}
async function createPackage(name: string, versions: string[] = ['1.0.0']): Promise<Package> {
const pkg = new Package();
pkg.id = Package.generateId('npm', testOrgId, name);
pkg.organizationId = testOrgId;
pkg.repositoryId = testRepoId;
pkg.protocol = 'npm';
pkg.name = name;
pkg.createdById = testUserId;
pkg.createdAt = new Date();
pkg.updatedAt = new Date();
for (const v of versions) {
pkg.addVersion(createVersion(v));
}
pkg.distTags['latest'] = versions[versions.length - 1];
await pkg.save();
return pkg;
}
describe('generateId', () => {
it('should generate correct format', () => {
const id = Package.generateId('npm', 'my-org', 'my-package');
assertEquals(id, 'npm:my-org:my-package');
});
});
describe('findById', () => {
it('should find package by ID', async () => {
const created = await createPackage('findable');
const found = await Package.findById(created.id);
assertExists(found);
assertEquals(found.name, 'findable');
});
it('should return null for non-existent ID', async () => {
const found = await Package.findById('npm:fake:package');
assertEquals(found, null);
});
});
describe('findByName', () => {
it('should find package by protocol, org, and name', async () => {
await createPackage('by-name');
const found = await Package.findByName('npm', testOrgId, 'by-name');
assertExists(found);
assertEquals(found.name, 'by-name');
});
});
describe('getOrgPackages', () => {
it('should return all packages in organization', async () => {
await createPackage('pkg1');
await createPackage('pkg2');
await createPackage('pkg3');
const packages = await Package.getOrgPackages(testOrgId);
assertEquals(packages.length, 3);
});
});
describe('search', () => {
it('should find packages by name', async () => {
await createPackage('search-me');
await createPackage('find-this');
await createPackage('other');
const results = await Package.search('search');
assertEquals(results.length, 1);
assertEquals(results[0].name, 'search-me');
});
it('should find packages by description', async () => {
const pkg = await createPackage('described');
pkg.description = 'A unique description for testing';
await pkg.save();
const results = await Package.search('unique description');
assertEquals(results.length, 1);
});
it('should filter by protocol', async () => {
await createPackage('npm-pkg');
const results = await Package.search('npm', { protocol: 'oci' });
assertEquals(results.length, 0);
});
it('should apply pagination', async () => {
await createPackage('page1');
await createPackage('page2');
await createPackage('page3');
const firstPage = await Package.search('page', { limit: 2, offset: 0 });
assertEquals(firstPage.length, 2);
const secondPage = await Package.search('page', { limit: 2, offset: 2 });
assertEquals(secondPage.length, 1);
});
});
describe('versions', () => {
it('should add version and update storage', async () => {
const pkg = await createPackage('versioned', []);
pkg.addVersion(createVersion('1.0.0'));
assertEquals(Object.keys(pkg.versions).length, 1);
assertEquals(pkg.storageBytes, 1024);
});
it('should get specific version', async () => {
const pkg = await createPackage('multi-version', ['1.0.0', '1.1.0', '2.0.0']);
const v1 = pkg.getVersion('1.0.0');
assertExists(v1);
assertEquals(v1.version, '1.0.0');
const v2 = pkg.getVersion('2.0.0');
assertExists(v2);
assertEquals(v2.version, '2.0.0');
});
it('should return undefined for non-existent version', async () => {
const pkg = await createPackage('single', ['1.0.0']);
const missing = pkg.getVersion('9.9.9');
assertEquals(missing, undefined);
});
});
describe('getLatestVersion', () => {
it('should return version from distTags.latest', async () => {
const pkg = await createPackage('tagged', ['1.0.0', '2.0.0']);
pkg.distTags['latest'] = '1.0.0'; // Set older version as latest
await pkg.save();
const latest = pkg.getLatestVersion();
assertExists(latest);
assertEquals(latest.version, '1.0.0');
});
it('should fallback to last version if no latest tag', async () => {
const pkg = await createPackage('untagged', ['1.0.0', '2.0.0']);
delete pkg.distTags['latest'];
const latest = pkg.getLatestVersion();
assertExists(latest);
assertEquals(latest.version, '2.0.0');
});
it('should return undefined for empty versions', async () => {
const pkg = await createPackage('empty', []);
delete pkg.distTags['latest'];
const latest = pkg.getLatestVersion();
assertEquals(latest, undefined);
});
});
describe('incrementDownloads', () => {
it('should increment total download count', async () => {
const pkg = await createPackage('downloads');
await pkg.incrementDownloads();
assertEquals(pkg.downloadCount, 1);
await pkg.incrementDownloads();
await pkg.incrementDownloads();
assertEquals(pkg.downloadCount, 3);
});
it('should increment version-specific downloads', async () => {
const pkg = await createPackage('version-downloads', ['1.0.0', '2.0.0']);
await pkg.incrementDownloads('1.0.0');
assertEquals(pkg.versions['1.0.0'].downloads, 1);
assertEquals(pkg.versions['2.0.0'].downloads, 0);
});
});
});

View File

@@ -0,0 +1,285 @@
/**
* Repository model unit tests
*/
import { assertEquals, assertExists, assertRejects } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import {
setupTestDb,
teardownTestDb,
cleanupTestDb,
createTestUser,
createOrgWithOwner,
} from '../../helpers/index.ts';
import { Repository } from '../../../ts/models/repository.ts';
describe('Repository Model', () => {
let testUserId: string;
let testOrgId: string;
beforeAll(async () => {
await setupTestDb();
});
afterAll(async () => {
await teardownTestDb();
});
beforeEach(async () => {
await cleanupTestDb();
const { user } = await createTestUser();
testUserId = user.id;
const { organization } = await createOrgWithOwner(testUserId);
testOrgId = organization.id;
});
describe('createRepository', () => {
it('should create a repository with valid data', async () => {
const repo = await Repository.createRepository({
organizationId: testOrgId,
name: 'test-repo',
description: 'A test repository',
protocol: 'npm',
createdById: testUserId,
});
assertExists(repo.id);
assertEquals(repo.name, 'test-repo');
assertEquals(repo.organizationId, testOrgId);
assertEquals(repo.protocol, 'npm');
assertEquals(repo.visibility, 'private');
assertEquals(repo.downloadCount, 0);
assertEquals(repo.starCount, 0);
});
it('should allow dots and underscores in name', async () => {
const repo = await Repository.createRepository({
organizationId: testOrgId,
name: 'my.test_repo',
protocol: 'npm',
createdById: testUserId,
});
assertEquals(repo.name, 'my.test_repo');
});
it('should lowercase the name', async () => {
const repo = await Repository.createRepository({
organizationId: testOrgId,
name: 'UPPERCASE',
protocol: 'npm',
createdById: testUserId,
});
assertEquals(repo.name, 'uppercase');
});
it('should set correct storage namespace', async () => {
const repo = await Repository.createRepository({
organizationId: testOrgId,
name: 'packages',
protocol: 'npm',
createdById: testUserId,
});
assertEquals(repo.storageNamespace, `npm/${testOrgId}/packages`);
});
it('should reject duplicate name+protocol in same org', async () => {
await Repository.createRepository({
organizationId: testOrgId,
name: 'unique',
protocol: 'npm',
createdById: testUserId,
});
await assertRejects(
async () => {
await Repository.createRepository({
organizationId: testOrgId,
name: 'unique',
protocol: 'npm',
createdById: testUserId,
});
},
Error,
'already exists'
);
});
it('should allow same name with different protocol', async () => {
await Repository.createRepository({
organizationId: testOrgId,
name: 'packages',
protocol: 'npm',
createdById: testUserId,
});
const ociRepo = await Repository.createRepository({
organizationId: testOrgId,
name: 'packages',
protocol: 'oci',
createdById: testUserId,
});
assertEquals(ociRepo.name, 'packages');
assertEquals(ociRepo.protocol, 'oci');
});
it('should reject invalid names', async () => {
await assertRejects(
async () => {
await Repository.createRepository({
organizationId: testOrgId,
name: '-invalid',
protocol: 'npm',
createdById: testUserId,
});
},
Error,
'lowercase alphanumeric'
);
});
it('should set visibility when provided', async () => {
const repo = await Repository.createRepository({
organizationId: testOrgId,
name: 'public-repo',
protocol: 'npm',
visibility: 'public',
createdById: testUserId,
});
assertEquals(repo.visibility, 'public');
});
});
describe('findByName', () => {
it('should find repository by org, name, and protocol', async () => {
await Repository.createRepository({
organizationId: testOrgId,
name: 'findable',
protocol: 'npm',
createdById: testUserId,
});
const found = await Repository.findByName(testOrgId, 'FINDABLE', 'npm');
assertExists(found);
assertEquals(found.name, 'findable');
});
it('should return null for wrong protocol', async () => {
await Repository.createRepository({
organizationId: testOrgId,
name: 'npm-only',
protocol: 'npm',
createdById: testUserId,
});
const found = await Repository.findByName(testOrgId, 'npm-only', 'oci');
assertEquals(found, null);
});
});
describe('getOrgRepositories', () => {
it('should return all org repositories', async () => {
await Repository.createRepository({
organizationId: testOrgId,
name: 'repo1',
protocol: 'npm',
createdById: testUserId,
});
await Repository.createRepository({
organizationId: testOrgId,
name: 'repo2',
protocol: 'oci',
createdById: testUserId,
});
await Repository.createRepository({
organizationId: testOrgId,
name: 'repo3',
protocol: 'maven',
createdById: testUserId,
});
const repos = await Repository.getOrgRepositories(testOrgId);
assertEquals(repos.length, 3);
});
});
describe('getPublicRepositories', () => {
it('should return only public repositories', async () => {
await Repository.createRepository({
organizationId: testOrgId,
name: 'public1',
protocol: 'npm',
visibility: 'public',
createdById: testUserId,
});
await Repository.createRepository({
organizationId: testOrgId,
name: 'private1',
protocol: 'npm',
visibility: 'private',
createdById: testUserId,
});
const repos = await Repository.getPublicRepositories();
assertEquals(repos.length, 1);
assertEquals(repos[0].name, 'public1');
});
it('should filter by protocol when provided', async () => {
await Repository.createRepository({
organizationId: testOrgId,
name: 'npm-public',
protocol: 'npm',
visibility: 'public',
createdById: testUserId,
});
await Repository.createRepository({
organizationId: testOrgId,
name: 'oci-public',
protocol: 'oci',
visibility: 'public',
createdById: testUserId,
});
const repos = await Repository.getPublicRepositories('npm');
assertEquals(repos.length, 1);
assertEquals(repos[0].protocol, 'npm');
});
});
describe('incrementDownloads', () => {
it('should increment download count', async () => {
const repo = await Repository.createRepository({
organizationId: testOrgId,
name: 'downloads',
protocol: 'npm',
createdById: testUserId,
});
await repo.incrementDownloads();
assertEquals(repo.downloadCount, 1);
await repo.incrementDownloads();
await repo.incrementDownloads();
assertEquals(repo.downloadCount, 3);
});
});
describe('getFullPath', () => {
it('should return org/repo path', async () => {
const repo = await Repository.createRepository({
organizationId: testOrgId,
name: 'my-package',
protocol: 'npm',
createdById: testUserId,
});
const path = repo.getFullPath('my-org');
assertEquals(path, 'my-org/my-package');
});
});
});

View File

@@ -0,0 +1,142 @@
/**
* Session model unit tests
*/
import { assertEquals, assertExists } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { setupTestDb, teardownTestDb, cleanupTestDb, createTestUser } from '../../helpers/index.ts';
import { Session } from '../../../ts/models/session.ts';
describe('Session Model', () => {
let testUserId: string;
beforeAll(async () => {
await setupTestDb();
});
afterAll(async () => {
await teardownTestDb();
});
beforeEach(async () => {
await cleanupTestDb();
const { user } = await createTestUser();
testUserId = user.id;
});
describe('createSession', () => {
it('should create a session with valid data', async () => {
const session = await Session.createSession({
userId: testUserId,
userAgent: 'Mozilla/5.0',
ipAddress: '192.168.1.1',
});
assertExists(session.id);
assertEquals(session.userId, testUserId);
assertEquals(session.userAgent, 'Mozilla/5.0');
assertEquals(session.ipAddress, '192.168.1.1');
assertEquals(session.isValid, true);
assertExists(session.createdAt);
assertExists(session.lastActivityAt);
});
});
describe('findValidSession', () => {
it('should find valid session by ID', async () => {
const created = await Session.createSession({
userId: testUserId,
userAgent: 'Test Agent',
ipAddress: '127.0.0.1',
});
const found = await Session.findValidSession(created.id);
assertExists(found);
assertEquals(found.id, created.id);
});
it('should not find invalidated session', async () => {
const session = await Session.createSession({
userId: testUserId,
userAgent: 'Test Agent',
ipAddress: '127.0.0.1',
});
await session.invalidate('Logged out');
const found = await Session.findValidSession(session.id);
assertEquals(found, null);
});
});
describe('getUserSessions', () => {
it('should return all valid sessions for user', async () => {
await Session.createSession({ userId: testUserId, userAgent: 'Agent 1', ipAddress: '1.1.1.1' });
await Session.createSession({ userId: testUserId, userAgent: 'Agent 2', ipAddress: '2.2.2.2' });
await Session.createSession({ userId: testUserId, userAgent: 'Agent 3', ipAddress: '3.3.3.3' });
const sessions = await Session.getUserSessions(testUserId);
assertEquals(sessions.length, 3);
});
it('should not return invalidated sessions', async () => {
await Session.createSession({ userId: testUserId, userAgent: 'Valid', ipAddress: '1.1.1.1' });
const invalid = await Session.createSession({
userId: testUserId,
userAgent: 'Invalid',
ipAddress: '2.2.2.2',
});
await invalid.invalidate('test');
const sessions = await Session.getUserSessions(testUserId);
assertEquals(sessions.length, 1);
});
});
describe('invalidate', () => {
it('should invalidate session with reason', async () => {
const session = await Session.createSession({
userId: testUserId,
userAgent: 'Test',
ipAddress: '127.0.0.1',
});
await session.invalidate('User logged out');
assertEquals(session.isValid, false);
assertExists(session.invalidatedAt);
assertEquals(session.invalidatedReason, 'User logged out');
});
});
describe('invalidateAllUserSessions', () => {
it('should invalidate all user sessions', async () => {
await Session.createSession({ userId: testUserId, userAgent: 'Agent 1', ipAddress: '1.1.1.1' });
await Session.createSession({ userId: testUserId, userAgent: 'Agent 2', ipAddress: '2.2.2.2' });
await Session.createSession({ userId: testUserId, userAgent: 'Agent 3', ipAddress: '3.3.3.3' });
const count = await Session.invalidateAllUserSessions(testUserId, 'Security logout');
assertEquals(count, 3);
const remaining = await Session.getUserSessions(testUserId);
assertEquals(remaining.length, 0);
});
});
describe('touchActivity', () => {
it('should update lastActivityAt', async () => {
const session = await Session.createSession({
userId: testUserId,
userAgent: 'Test',
ipAddress: '127.0.0.1',
});
const originalActivity = session.lastActivityAt;
// Wait a bit to ensure time difference
await new Promise((r) => setTimeout(r, 10));
await session.touchActivity();
assertEquals(session.lastActivityAt > originalActivity, true);
});
});
});

View File

@@ -0,0 +1,228 @@
/**
* User model unit tests
*/
import { assertEquals, assertExists, assertRejects } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { setupTestDb, teardownTestDb, cleanupTestDb } from '../../helpers/index.ts';
import { User } from '../../../ts/models/user.ts';
describe('User Model', () => {
beforeAll(async () => {
await setupTestDb();
});
afterAll(async () => {
await teardownTestDb();
});
beforeEach(async () => {
await cleanupTestDb();
});
describe('createUser', () => {
it('should create a user with valid data', async () => {
const passwordHash = await User.hashPassword('testpassword');
const user = await User.createUser({
email: 'test@example.com',
username: 'testuser',
passwordHash,
displayName: 'Test User',
});
assertExists(user.id);
assertEquals(user.email, 'test@example.com');
assertEquals(user.username, 'testuser');
assertEquals(user.displayName, 'Test User');
assertEquals(user.status, 'pending_verification');
assertEquals(user.emailVerified, false);
assertEquals(user.isPlatformAdmin, false);
});
it('should lowercase email and username', async () => {
const passwordHash = await User.hashPassword('testpassword');
const user = await User.createUser({
email: 'TEST@EXAMPLE.COM',
username: 'TestUser',
passwordHash,
});
assertEquals(user.email, 'test@example.com');
assertEquals(user.username, 'testuser');
});
it('should use username as displayName if not provided', async () => {
const passwordHash = await User.hashPassword('testpassword');
const user = await User.createUser({
email: 'test2@example.com',
username: 'testuser2',
passwordHash,
});
assertEquals(user.displayName, 'testuser2');
});
});
describe('findByEmail', () => {
it('should find user by email (case-insensitive)', async () => {
const passwordHash = await User.hashPassword('testpassword');
await User.createUser({
email: 'findme@example.com',
username: 'findme',
passwordHash,
});
const found = await User.findByEmail('FINDME@example.com');
assertExists(found);
assertEquals(found.email, 'findme@example.com');
});
it('should return null for non-existent email', async () => {
const found = await User.findByEmail('nonexistent@example.com');
assertEquals(found, null);
});
});
describe('findByUsername', () => {
it('should find user by username (case-insensitive)', async () => {
const passwordHash = await User.hashPassword('testpassword');
await User.createUser({
email: 'user@example.com',
username: 'findbyname',
passwordHash,
});
const found = await User.findByUsername('FINDBYNAME');
assertExists(found);
assertEquals(found.username, 'findbyname');
});
});
describe('findById', () => {
it('should find user by ID', async () => {
const passwordHash = await User.hashPassword('testpassword');
const created = await User.createUser({
email: 'byid@example.com',
username: 'byid',
passwordHash,
});
const found = await User.findById(created.id);
assertExists(found);
assertEquals(found.id, created.id);
});
});
describe('password hashing', () => {
it('should hash password with salt', async () => {
const hash = await User.hashPassword('mypassword');
assertExists(hash);
assertEquals(hash.includes(':'), true);
const [salt, _hashPart] = hash.split(':');
assertEquals(salt.length, 32); // 16 bytes = 32 hex chars
});
it('should produce different hashes for same password', async () => {
const hash1 = await User.hashPassword('samepassword');
const hash2 = await User.hashPassword('samepassword');
// Different salts should produce different hashes
assertEquals(hash1 !== hash2, true);
});
});
describe('verifyPassword', () => {
it('should verify correct password', async () => {
const passwordHash = await User.hashPassword('correctpassword');
const user = await User.createUser({
email: 'verify@example.com',
username: 'verifyuser',
passwordHash,
});
const isValid = await user.verifyPassword('correctpassword');
assertEquals(isValid, true);
});
it('should reject incorrect password', async () => {
const passwordHash = await User.hashPassword('correctpassword');
const user = await User.createUser({
email: 'reject@example.com',
username: 'rejectuser',
passwordHash,
});
const isValid = await user.verifyPassword('wrongpassword');
assertEquals(isValid, false);
});
it('should reject empty password', async () => {
const passwordHash = await User.hashPassword('correctpassword');
const user = await User.createUser({
email: 'empty@example.com',
username: 'emptyuser',
passwordHash,
});
const isValid = await user.verifyPassword('');
assertEquals(isValid, false);
});
});
describe('isActive', () => {
it('should return true for active status', async () => {
const passwordHash = await User.hashPassword('test');
const user = await User.createUser({
email: 'active@example.com',
username: 'activeuser',
passwordHash,
});
user.status = 'active';
await user.save();
assertEquals(user.isActive, true);
});
it('should return false for suspended status', async () => {
const passwordHash = await User.hashPassword('test');
const user = await User.createUser({
email: 'suspended@example.com',
username: 'suspendeduser',
passwordHash,
});
user.status = 'suspended';
assertEquals(user.isActive, false);
});
});
describe('isPlatformAdmin', () => {
it('should default to false', async () => {
const passwordHash = await User.hashPassword('test');
const user = await User.createUser({
email: 'notadmin@example.com',
username: 'notadmin',
passwordHash,
});
assertEquals(user.isPlatformAdmin, false);
assertEquals(user.isSystemAdmin, false);
});
it('should be settable to true', async () => {
const passwordHash = await User.hashPassword('test');
const user = await User.createUser({
email: 'admin@example.com',
username: 'adminuser',
passwordHash,
});
user.isPlatformAdmin = true;
await user.save();
const found = await User.findById(user.id);
assertEquals(found!.isPlatformAdmin, true);
assertEquals(found!.isSystemAdmin, true);
});
});
});

View File

@@ -0,0 +1,224 @@
/**
* AuthService unit tests
*/
import { assertEquals, assertExists } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { setupTestDb, teardownTestDb, cleanupTestDb, createTestUser } from '../../helpers/index.ts';
import { AuthService } from '../../../ts/services/auth.service.ts';
import { Session } from '../../../ts/models/session.ts';
import { testConfig } from '../../test.config.ts';
describe('AuthService', () => {
let authService: AuthService;
beforeAll(async () => {
await setupTestDb();
authService = new AuthService({
jwtSecret: testConfig.jwt.secret,
accessTokenExpiresIn: 60, // 1 minute for tests
refreshTokenExpiresIn: 300, // 5 minutes for tests
});
});
afterAll(async () => {
await teardownTestDb();
});
beforeEach(async () => {
await cleanupTestDb();
});
describe('login', () => {
it('should successfully login with valid credentials', async () => {
const { user, password } = await createTestUser({
email: 'login@example.com',
status: 'active',
});
const result = await authService.login(user.email, password, {
userAgent: 'TestAgent/1.0',
ipAddress: '127.0.0.1',
});
assertEquals(result.success, true);
assertExists(result.user);
assertEquals(result.user.id, user.id);
assertExists(result.accessToken);
assertExists(result.refreshToken);
assertExists(result.sessionId);
});
it('should fail with invalid email', async () => {
const result = await authService.login('nonexistent@example.com', 'password');
assertEquals(result.success, false);
assertEquals(result.errorCode, 'INVALID_CREDENTIALS');
});
it('should fail with invalid password', async () => {
const { user } = await createTestUser({ email: 'wrongpass@example.com' });
const result = await authService.login(user.email, 'wrongpassword');
assertEquals(result.success, false);
assertEquals(result.errorCode, 'INVALID_CREDENTIALS');
});
it('should fail for inactive user', async () => {
const { user, password } = await createTestUser({
email: 'inactive@example.com',
status: 'suspended',
});
const result = await authService.login(user.email, password);
assertEquals(result.success, false);
assertEquals(result.errorCode, 'ACCOUNT_INACTIVE');
});
it('should create a session on successful login', async () => {
const { user, password } = await createTestUser({ email: 'session@example.com' });
const result = await authService.login(user.email, password);
assertEquals(result.success, true);
assertExists(result.sessionId);
const session = await Session.findValidSession(result.sessionId!);
assertExists(session);
assertEquals(session.userId, user.id);
});
});
describe('refresh', () => {
it('should refresh access token with valid refresh token', async () => {
const { user, password } = await createTestUser({ email: 'refresh@example.com' });
const loginResult = await authService.login(user.email, password);
assertEquals(loginResult.success, true);
const refreshResult = await authService.refresh(loginResult.refreshToken!);
assertEquals(refreshResult.success, true);
assertExists(refreshResult.accessToken);
assertEquals(refreshResult.sessionId, loginResult.sessionId);
});
it('should fail with invalid refresh token', async () => {
const result = await authService.refresh('invalid-token');
assertEquals(result.success, false);
assertEquals(result.errorCode, 'INVALID_TOKEN');
});
it('should fail when session is invalidated', async () => {
const { user, password } = await createTestUser({ email: 'invalidsession@example.com' });
const loginResult = await authService.login(user.email, password);
// Invalidate session
const session = await Session.findValidSession(loginResult.sessionId!);
await session!.invalidate('test');
const refreshResult = await authService.refresh(loginResult.refreshToken!);
assertEquals(refreshResult.success, false);
assertEquals(refreshResult.errorCode, 'SESSION_INVALID');
});
});
describe('validateAccessToken', () => {
it('should validate valid access token', async () => {
const { user, password } = await createTestUser({ email: 'validate@example.com' });
const loginResult = await authService.login(user.email, password);
const validation = await authService.validateAccessToken(loginResult.accessToken!);
assertExists(validation);
assertEquals(validation.user.id, user.id);
assertEquals(validation.sessionId, loginResult.sessionId);
});
it('should reject invalid access token', async () => {
const validation = await authService.validateAccessToken('invalid-token');
assertEquals(validation, null);
});
it('should reject when session is invalidated', async () => {
const { user, password } = await createTestUser({ email: 'invalidated@example.com' });
const loginResult = await authService.login(user.email, password);
// Invalidate session
const session = await Session.findValidSession(loginResult.sessionId!);
await session!.invalidate('test');
const validation = await authService.validateAccessToken(loginResult.accessToken!);
assertEquals(validation, null);
});
});
describe('logout', () => {
it('should invalidate session', async () => {
const { user, password } = await createTestUser({ email: 'logout@example.com' });
const loginResult = await authService.login(user.email, password);
const success = await authService.logout(loginResult.sessionId!);
assertEquals(success, true);
const session = await Session.findValidSession(loginResult.sessionId!);
assertEquals(session, null);
});
it('should return false for non-existent session', async () => {
const success = await authService.logout('non-existent-session-id');
assertEquals(success, false);
});
});
describe('logoutAll', () => {
it('should invalidate all user sessions', async () => {
const { user, password } = await createTestUser({ email: 'logoutall@example.com' });
// Create multiple sessions
await authService.login(user.email, password);
await authService.login(user.email, password);
await authService.login(user.email, password);
const count = await authService.logoutAll(user.id);
assertEquals(count, 3);
const sessions = await Session.getUserSessions(user.id);
assertEquals(sessions.length, 0);
});
});
describe('static password methods', () => {
it('should hash and verify password', async () => {
const password = 'MySecurePassword123!';
const hash = await AuthService.hashPassword(password);
const isValid = await AuthService.verifyPassword(password, hash);
assertEquals(isValid, true);
const isInvalid = await AuthService.verifyPassword('WrongPassword', hash);
assertEquals(isInvalid, false);
});
it('should generate different hashes for same password', async () => {
const password = 'SamePassword';
const hash1 = await AuthService.hashPassword(password);
const hash2 = await AuthService.hashPassword(password);
assertEquals(hash1 !== hash2, true);
// But both should verify
assertEquals(await AuthService.verifyPassword(password, hash1), true);
assertEquals(await AuthService.verifyPassword(password, hash2), true);
});
});
});

View File

@@ -0,0 +1,260 @@
/**
* TokenService unit tests
*/
import { assertEquals, assertExists, assertMatch } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { setupTestDb, teardownTestDb, cleanupTestDb, createTestUser } from '../../helpers/index.ts';
import { TokenService } from '../../../ts/services/token.service.ts';
import { ApiToken } from '../../../ts/models/apitoken.ts';
describe('TokenService', () => {
let tokenService: TokenService;
let testUserId: string;
beforeAll(async () => {
await setupTestDb();
tokenService = new TokenService();
});
afterAll(async () => {
await teardownTestDb();
});
beforeEach(async () => {
await cleanupTestDb();
const { user } = await createTestUser();
testUserId = user.id;
});
describe('createToken', () => {
it('should create token with correct format', async () => {
const result = await tokenService.createToken({
userId: testUserId,
name: 'test-token',
protocols: ['npm', 'oci'],
scopes: [{ protocol: '*', actions: ['read', 'write'] }],
});
assertExists(result.rawToken);
assertExists(result.token);
// Check token format: srg_{prefix}_{random}
assertMatch(result.rawToken, /^srg_[a-z0-9]+_[a-z0-9]+$/);
assertEquals(result.token.name, 'test-token');
assertEquals(result.token.protocols.includes('npm'), true);
assertEquals(result.token.protocols.includes('oci'), true);
});
it('should store hashed token', async () => {
const result = await tokenService.createToken({
userId: testUserId,
name: 'hashed-token',
protocols: ['npm'],
scopes: [{ protocol: 'npm', actions: ['read'] }],
});
// The stored token should be hashed
assertEquals(result.token.tokenHash !== result.rawToken, true);
assertEquals(result.token.tokenHash.length, 64); // SHA-256 hex
});
it('should set expiration when provided', async () => {
const result = await tokenService.createToken({
userId: testUserId,
name: 'expiring-token',
protocols: ['npm'],
scopes: [{ protocol: 'npm', actions: ['read'] }],
expiresInDays: 30,
});
assertExists(result.token.expiresAt);
const expectedExpiry = new Date();
expectedExpiry.setDate(expectedExpiry.getDate() + 30);
// Should be within a few seconds of expected
const diff = Math.abs(result.token.expiresAt.getTime() - expectedExpiry.getTime());
assertEquals(diff < 5000, true);
});
it('should create org-owned token', async () => {
const orgId = 'test-org-123';
const result = await tokenService.createToken({
userId: testUserId,
organizationId: orgId,
name: 'org-token',
protocols: ['npm'],
scopes: [{ protocol: 'npm', organizationId: orgId, actions: ['read', 'write'] }],
});
assertEquals(result.token.organizationId, orgId);
});
});
describe('validateToken', () => {
it('should validate correct token', async () => {
const { rawToken } = await tokenService.createToken({
userId: testUserId,
name: 'valid-token',
protocols: ['npm'],
scopes: [{ protocol: 'npm', actions: ['read'] }],
});
const validation = await tokenService.validateToken(rawToken, '127.0.0.1');
assertExists(validation);
assertEquals(validation.userId, testUserId);
assertEquals(validation.protocols.includes('npm'), true);
});
it('should reject invalid token format', async () => {
const validation = await tokenService.validateToken('invalid-format', '127.0.0.1');
assertEquals(validation, null);
});
it('should reject non-existent token', async () => {
const validation = await tokenService.validateToken('srg_abc123_def456', '127.0.0.1');
assertEquals(validation, null);
});
it('should reject revoked token', async () => {
const { rawToken, token } = await tokenService.createToken({
userId: testUserId,
name: 'revoked-token',
protocols: ['npm'],
scopes: [{ protocol: 'npm', actions: ['read'] }],
});
await token.revoke('Test revocation');
const validation = await tokenService.validateToken(rawToken, '127.0.0.1');
assertEquals(validation, null);
});
it('should reject expired token', async () => {
const { rawToken, token } = await tokenService.createToken({
userId: testUserId,
name: 'expired-token',
protocols: ['npm'],
scopes: [{ protocol: 'npm', actions: ['read'] }],
expiresInDays: 1,
});
// Manually set expiry to past
token.expiresAt = new Date(Date.now() - 86400000);
await token.save();
const validation = await tokenService.validateToken(rawToken, '127.0.0.1');
assertEquals(validation, null);
});
it('should record usage on validation', async () => {
const { rawToken, token } = await tokenService.createToken({
userId: testUserId,
name: 'usage-token',
protocols: ['npm'],
scopes: [{ protocol: 'npm', actions: ['read'] }],
});
await tokenService.validateToken(rawToken, '192.168.1.100');
// Reload token from DB
const updated = await ApiToken.findByHash(token.tokenHash);
assertExists(updated);
assertExists(updated.lastUsedAt);
assertEquals(updated.lastUsedIp, '192.168.1.100');
assertEquals(updated.usageCount, 1);
});
});
describe('getUserTokens', () => {
it('should return all user tokens', async () => {
await tokenService.createToken({
userId: testUserId,
name: 'token1',
protocols: ['npm'],
scopes: [{ protocol: 'npm', actions: ['read'] }],
});
await tokenService.createToken({
userId: testUserId,
name: 'token2',
protocols: ['oci'],
scopes: [{ protocol: 'oci', actions: ['read'] }],
});
const tokens = await tokenService.getUserTokens(testUserId);
assertEquals(tokens.length, 2);
});
it('should not return revoked tokens', async () => {
const { token } = await tokenService.createToken({
userId: testUserId,
name: 'revoked',
protocols: ['npm'],
scopes: [{ protocol: 'npm', actions: ['read'] }],
});
await tokenService.createToken({
userId: testUserId,
name: 'active',
protocols: ['npm'],
scopes: [{ protocol: 'npm', actions: ['read'] }],
});
await token.revoke('test');
const tokens = await tokenService.getUserTokens(testUserId);
assertEquals(tokens.length, 1);
assertEquals(tokens[0].name, 'active');
});
});
describe('revokeToken', () => {
it('should revoke token with reason', async () => {
const { token } = await tokenService.createToken({
userId: testUserId,
name: 'to-revoke',
protocols: ['npm'],
scopes: [{ protocol: 'npm', actions: ['read'] }],
});
await tokenService.revokeToken(token.id, 'Security concern');
const updated = await ApiToken.findByPrefix(token.tokenPrefix);
assertExists(updated);
assertEquals(updated.isRevoked, true);
assertEquals(updated.revokedReason, 'Security concern');
});
});
describe('getOrgTokens', () => {
it('should return organization tokens', async () => {
const orgId = 'org-123';
await tokenService.createToken({
userId: testUserId,
organizationId: orgId,
name: 'org-token',
protocols: ['npm'],
scopes: [{ protocol: 'npm', actions: ['read'] }],
});
await tokenService.createToken({
userId: testUserId,
name: 'personal-token',
protocols: ['npm'],
scopes: [{ protocol: 'npm', actions: ['read'] }],
});
const tokens = await tokenService.getOrgTokens(orgId);
assertEquals(tokens.length, 1);
assertEquals(tokens[0].organizationId, orgId);
});
});
});