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

@@ -15,6 +15,15 @@ export class OrganizationApi {
this.permissionService = permissionService;
}
/**
* Helper to resolve organization by ID or name
*/
private async resolveOrganization(idOrName: string): Promise<Organization | null> {
return idOrName.startsWith('Organization:')
? await Organization.findById(idOrName)
: await Organization.findByName(idOrName);
}
/**
* GET /api/v1/organizations
*/
@@ -56,19 +65,20 @@ export class OrganizationApi {
/**
* GET /api/v1/organizations/:id
* Supports lookup by ID (e.g., Organization:abc123) or by name (e.g., push.rocks)
*/
public async get(ctx: IApiContext): Promise<IApiResponse> {
const { id } = ctx.params;
try {
const org = await Organization.findById(id);
const org = await this.resolveOrganization(id);
if (!org) {
return { status: 404, body: { error: 'Organization not found' } };
}
// Check access - public orgs are visible to all authenticated users
if (!org.isPublic && ctx.actor?.userId) {
const isMember = await OrganizationMember.findMembership(id, ctx.actor.userId);
const isMember = await OrganizationMember.findMembership(org.id, ctx.actor.userId);
if (!isMember && !ctx.actor.user?.isSystemAdmin) {
return { status: 403, body: { error: 'Access denied' } };
}
@@ -112,11 +122,11 @@ export class OrganizationApi {
return { status: 400, body: { error: 'Organization name is required' } };
}
// Validate name format
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(name)) {
// Validate name format (allows dots for domain-like names)
if (!/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/.test(name)) {
return {
status: 400,
body: { error: 'Name must be lowercase alphanumeric with optional hyphens' },
body: { error: 'Name must be lowercase alphanumeric with optional hyphens and dots' },
};
}
@@ -176,6 +186,7 @@ export class OrganizationApi {
/**
* PUT /api/v1/organizations/:id
* Supports lookup by ID or name
*/
public async update(ctx: IApiContext): Promise<IApiResponse> {
if (!ctx.actor?.userId) {
@@ -184,18 +195,18 @@ export class OrganizationApi {
const { id } = ctx.params;
// Check admin permission
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, id);
if (!canManage) {
return { status: 403, body: { error: 'Admin access required' } };
}
try {
const org = await Organization.findById(id);
const org = await this.resolveOrganization(id);
if (!org) {
return { status: 404, body: { error: 'Organization not found' } };
}
// Check admin permission using org.id
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, org.id);
if (!canManage) {
return { status: 403, body: { error: 'Admin access required' } };
}
const body = await ctx.request.json();
const { displayName, description, avatarUrl, website, isPublic, settings } = body;
@@ -232,6 +243,7 @@ export class OrganizationApi {
/**
* DELETE /api/v1/organizations/:id
* Supports lookup by ID or name
*/
public async delete(ctx: IApiContext): Promise<IApiResponse> {
if (!ctx.actor?.userId) {
@@ -240,18 +252,18 @@ export class OrganizationApi {
const { id } = ctx.params;
// Only owners and system admins can delete
const membership = await OrganizationMember.findMembership(id, ctx.actor.userId);
if (membership?.role !== 'owner' && !ctx.actor.user?.isSystemAdmin) {
return { status: 403, body: { error: 'Owner access required' } };
}
try {
const org = await Organization.findById(id);
const org = await this.resolveOrganization(id);
if (!org) {
return { status: 404, body: { error: 'Organization not found' } };
}
// Only owners and system admins can delete
const membership = await OrganizationMember.findMembership(org.id, ctx.actor.userId);
if (membership?.role !== 'owner' && !ctx.actor.user?.isSystemAdmin) {
return { status: 403, body: { error: 'Owner access required' } };
}
// TODO: Check for packages, repositories before deletion
// For now, just delete the organization and memberships
await org.delete();
@@ -268,6 +280,7 @@ export class OrganizationApi {
/**
* GET /api/v1/organizations/:id/members
* Supports lookup by ID or name
*/
public async listMembers(ctx: IApiContext): Promise<IApiResponse> {
if (!ctx.actor?.userId) {
@@ -276,14 +289,19 @@ export class OrganizationApi {
const { id } = ctx.params;
// Check membership
const isMember = await OrganizationMember.findMembership(id, ctx.actor.userId);
if (!isMember && !ctx.actor.user?.isSystemAdmin) {
return { status: 403, body: { error: 'Access denied' } };
}
try {
const members = await OrganizationMember.getOrgMembers(id);
const org = await this.resolveOrganization(id);
if (!org) {
return { status: 404, body: { error: 'Organization not found' } };
}
// Check membership
const isMember = await OrganizationMember.findMembership(org.id, ctx.actor.userId);
if (!isMember && !ctx.actor.user?.isSystemAdmin) {
return { status: 403, body: { error: 'Access denied' } };
}
const members = await OrganizationMember.getOrgMembers(org.id);
// Fetch user details
const membersWithUsers = await Promise.all(
@@ -316,6 +334,7 @@ export class OrganizationApi {
/**
* POST /api/v1/organizations/:id/members
* Supports lookup by ID or name
*/
public async addMember(ctx: IApiContext): Promise<IApiResponse> {
if (!ctx.actor?.userId) {
@@ -324,13 +343,18 @@ export class OrganizationApi {
const { id } = ctx.params;
// Check admin permission
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, id);
if (!canManage) {
return { status: 403, body: { error: 'Admin access required' } };
}
try {
const org = await this.resolveOrganization(id);
if (!org) {
return { status: 404, body: { error: 'Organization not found' } };
}
// Check admin permission
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, org.id);
if (!canManage) {
return { status: 403, body: { error: 'Admin access required' } };
}
const body = await ctx.request.json();
const { userId, role } = body as { userId: string; role: TOrganizationRole };
@@ -349,7 +373,7 @@ export class OrganizationApi {
}
// Check if already a member
const existing = await OrganizationMember.findMembership(id, userId);
const existing = await OrganizationMember.findMembership(org.id, userId);
if (existing) {
return { status: 409, body: { error: 'User is already a member' } };
}
@@ -357,7 +381,7 @@ export class OrganizationApi {
// Add member
const membership = new OrganizationMember();
membership.id = await OrganizationMember.getNewId();
membership.organizationId = id;
membership.organizationId = org.id;
membership.userId = userId;
membership.role = role;
membership.addedById = ctx.actor.userId;
@@ -366,11 +390,8 @@ export class OrganizationApi {
await membership.save();
// Update member count
const org = await Organization.findById(id);
if (org) {
org.memberCount += 1;
await org.save();
}
org.memberCount += 1;
await org.save();
return {
status: 201,
@@ -388,6 +409,7 @@ export class OrganizationApi {
/**
* PUT /api/v1/organizations/:id/members/:userId
* Supports lookup by ID or name
*/
public async updateMember(ctx: IApiContext): Promise<IApiResponse> {
if (!ctx.actor?.userId) {
@@ -396,13 +418,18 @@ export class OrganizationApi {
const { id, userId } = ctx.params;
// Check admin permission
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, id);
if (!canManage) {
return { status: 403, body: { error: 'Admin access required' } };
}
try {
const org = await this.resolveOrganization(id);
if (!org) {
return { status: 404, body: { error: 'Organization not found' } };
}
// Check admin permission
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, org.id);
if (!canManage) {
return { status: 403, body: { error: 'Admin access required' } };
}
const body = await ctx.request.json();
const { role } = body as { role: TOrganizationRole };
@@ -410,14 +437,14 @@ export class OrganizationApi {
return { status: 400, body: { error: 'Valid role is required' } };
}
const membership = await OrganizationMember.findMembership(id, userId);
const membership = await OrganizationMember.findMembership(org.id, userId);
if (!membership) {
return { status: 404, body: { error: 'Member not found' } };
}
// Cannot change last owner
if (membership.role === 'owner' && role !== 'owner') {
const owners = await OrganizationMember.getOrgMembers(id);
const owners = await OrganizationMember.getOrgMembers(org.id);
const ownerCount = owners.filter((m) => m.role === 'owner').length;
if (ownerCount <= 1) {
return { status: 400, body: { error: 'Cannot remove the last owner' } };
@@ -442,6 +469,7 @@ export class OrganizationApi {
/**
* DELETE /api/v1/organizations/:id/members/:userId
* Supports lookup by ID or name
*/
public async removeMember(ctx: IApiContext): Promise<IApiResponse> {
if (!ctx.actor?.userId) {
@@ -450,23 +478,28 @@ export class OrganizationApi {
const { id, userId } = ctx.params;
// Users can remove themselves, admins can remove others
if (userId !== ctx.actor.userId) {
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, id);
if (!canManage) {
return { status: 403, body: { error: 'Admin access required' } };
}
}
try {
const membership = await OrganizationMember.findMembership(id, userId);
const org = await this.resolveOrganization(id);
if (!org) {
return { status: 404, body: { error: 'Organization not found' } };
}
// Users can remove themselves, admins can remove others
if (userId !== ctx.actor.userId) {
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, org.id);
if (!canManage) {
return { status: 403, body: { error: 'Admin access required' } };
}
}
const membership = await OrganizationMember.findMembership(org.id, userId);
if (!membership) {
return { status: 404, body: { error: 'Member not found' } };
}
// Cannot remove last owner
if (membership.role === 'owner') {
const owners = await OrganizationMember.getOrgMembers(id);
const owners = await OrganizationMember.getOrgMembers(org.id);
const ownerCount = owners.filter((m) => m.role === 'owner').length;
if (ownerCount <= 1) {
return { status: 400, body: { error: 'Cannot remove the last owner' } };
@@ -476,11 +509,8 @@ export class OrganizationApi {
await membership.delete();
// Update member count
const org = await Organization.findById(id);
if (org) {
org.memberCount = Math.max(0, org.memberCount - 1);
await org.save();
}
org.memberCount = Math.max(0, org.memberCount - 1);
await org.save();
return {
status: 200,