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);
});
});
});