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

@@ -38,14 +38,14 @@ export const routes: Routes = [
),
},
{
path: ':orgId',
path: ':orgName',
loadComponent: () =>
import('./features/organizations/organization-detail.component').then(
(m) => m.OrganizationDetailComponent
),
},
{
path: ':orgId/repositories/:repoId',
path: ':orgName/repositories/:repoId',
loadComponent: () =>
import('./features/repositories/repository-detail.component').then(
(m) => m.RepositoryDetailComponent

View File

@@ -11,7 +11,7 @@ import { ToastService } from '../../core/services/toast.service';
<div class="p-6 max-w-7xl mx-auto">
@if (loading()) {
<div class="flex items-center justify-center py-12">
<svg class="animate-spin h-8 w-8 text-primary-600" fill="none" viewBox="0 0 24 24">
<svg class="animate-spin h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
@@ -20,33 +20,36 @@ import { ToastService } from '../../core/services/toast.service';
<!-- Header -->
<div class="flex items-start justify-between mb-8">
<div class="flex items-center gap-4">
<div class="w-16 h-16 bg-gray-200 dark:bg-gray-700 rounded-xl flex items-center justify-center">
<span class="text-2xl font-medium text-gray-600 dark:text-gray-300">
<div class="w-16 h-16 bg-muted flex items-center justify-center">
<span class="font-mono text-2xl font-medium text-muted-foreground">
{{ organization()!.name.charAt(0).toUpperCase() }}
</span>
</div>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ organization()!.displayName }}</h1>
<p class="text-gray-500 dark:text-gray-400">&#64;{{ organization()!.name }}</p>
<h1 class="font-mono text-2xl font-bold text-foreground">{{ organization()!.displayName }}</h1>
<p class="font-mono text-muted-foreground">&#64;{{ organization()!.name }}</p>
</div>
</div>
<div class="flex items-center gap-3">
@if (organization()!.isPublic) {
<span class="badge-default">Public</span>
<span class="badge-accent">Public</span>
} @else {
<span class="badge-warning">Private</span>
<span class="badge-primary">Private</span>
}
</div>
</div>
@if (organization()!.description) {
<p class="text-gray-600 dark:text-gray-400 mb-8">{{ organization()!.description }}</p>
<p class="font-mono text-muted-foreground mb-8">{{ organization()!.description }}</p>
}
<!-- Repositories Section -->
<div class="mb-8">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Repositories</h2>
<div class="section-header">
<div class="section-indicator"></div>
<span class="section-label">Repositories</span>
</div>
<button class="btn-primary btn-sm">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
@@ -57,26 +60,26 @@ import { ToastService } from '../../core/services/toast.service';
@if (repositories().length === 0) {
<div class="card card-content text-center py-8">
<p class="text-gray-500 dark:text-gray-400">No repositories yet</p>
<p class="font-mono text-muted-foreground">No repositories yet</p>
</div>
} @else {
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@for (repo of repositories(); track repo.id) {
<a [routerLink]="['repositories', repo.id]" class="card card-content hover:border-primary-300 dark:hover:border-primary-700 transition-colors">
<a [routerLink]="['repositories', repo.id]" class="card card-content hover:border-primary/50 transition-colors">
<div class="flex items-start justify-between">
<div>
<h3 class="font-medium text-gray-900 dark:text-gray-100">{{ repo.displayName }}</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ repo.name }}</p>
<h3 class="font-mono font-medium text-foreground">{{ repo.displayName }}</h3>
<p class="font-mono text-sm text-muted-foreground">{{ repo.name }}</p>
</div>
@if (repo.isPublic) {
<span class="badge-default">Public</span>
<span class="badge-accent">Public</span>
}
</div>
@if (repo.description) {
<p class="text-sm text-gray-600 dark:text-gray-400 mt-2 line-clamp-2">{{ repo.description }}</p>
<p class="font-mono text-sm text-muted-foreground mt-2 line-clamp-2">{{ repo.description }}</p>
}
<div class="mt-3 flex items-center gap-4">
<div class="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<div class="flex items-center gap-1 font-mono text-sm text-muted-foreground">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
@@ -95,18 +98,24 @@ import { ToastService } from '../../core/services/toast.service';
</div>
<!-- Stats -->
<div class="mb-4">
<div class="section-header">
<div class="section-indicator"></div>
<span class="section-label">Statistics</span>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="card card-content">
<p class="text-sm text-gray-500 dark:text-gray-400">Members</p>
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ organization()!.memberCount }}</p>
<p class="font-mono text-sm text-muted-foreground">Members</p>
<p class="font-mono text-2xl font-bold text-foreground">{{ organization()!.memberCount }}</p>
</div>
<div class="card card-content">
<p class="text-sm text-gray-500 dark:text-gray-400">Repositories</p>
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ repositories().length }}</p>
<p class="font-mono text-sm text-muted-foreground">Repositories</p>
<p class="font-mono text-2xl font-bold text-foreground">{{ repositories().length }}</p>
</div>
<div class="card card-content">
<p class="text-sm text-gray-500 dark:text-gray-400">Created</p>
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ formatDate(organization()!.createdAt) }}</p>
<p class="font-mono text-sm text-muted-foreground">Created</p>
<p class="font-mono text-2xl font-bold text-foreground">{{ formatDate(organization()!.createdAt) }}</p>
</div>
</div>
}
@@ -123,18 +132,18 @@ export class OrganizationDetailComponent implements OnInit {
loading = signal(true);
ngOnInit(): void {
const orgId = this.route.snapshot.paramMap.get('orgId');
if (orgId) {
this.loadData(orgId);
const orgName = this.route.snapshot.paramMap.get('orgName');
if (orgName) {
this.loadData(orgName);
}
}
private async loadData(orgId: string): Promise<void> {
private async loadData(orgName: string): Promise<void> {
this.loading.set(true);
try {
const [org, reposResponse] = await Promise.all([
this.apiService.getOrganization(orgId).toPromise(),
this.apiService.getRepositories(orgId).toPromise(),
this.apiService.getOrganization(orgName).toPromise(),
this.apiService.getRepositories(orgName).toPromise(),
]);
this.organization.set(org || null);
this.repositories.set(reposResponse?.repositories || []);

View File

@@ -47,7 +47,7 @@ import { ToastService } from '../../core/services/toast.service';
} @else {
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@for (org of organizations(); track org.id) {
<a [routerLink]="['/organizations', org.id]" class="card hover:border-primary/50 transition-colors">
<a [routerLink]="['/organizations', org.name]" class="card hover:border-primary/50 transition-colors">
<div class="card-content">
<div class="flex items-start gap-4">
<div class="w-12 h-12 bg-muted flex items-center justify-center flex-shrink-0">
@@ -84,8 +84,8 @@ import { ToastService } from '../../core/services/toast.service';
<!-- Create Modal -->
@if (showCreateModal()) {
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
<div class="card w-full max-w-md mx-4">
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 modal-backdrop">
<div class="card w-full max-w-md mx-4 modal-content">
<div class="card-header flex items-center justify-between">
<div class="section-header">
<div class="section-indicator"></div>
@@ -105,20 +105,20 @@ import { ToastService } from '../../core/services/toast.service';
[(ngModel)]="newOrg.name"
name="name"
class="input"
placeholder="my-organization"
placeholder="push.rocks"
required
pattern="^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
pattern="^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$"
/>
<p class="font-mono text-xs text-muted-foreground mt-1">Lowercase letters, numbers, and hyphens only</p>
<p class="font-mono text-xs text-muted-foreground mt-1">Lowercase letters, numbers, hyphens, and dots (e.g., push.rocks)</p>
</div>
<div>
<label class="label block mb-1.5">Display Name</label>
<label class="label block mb-1.5">Display Name (optional)</label>
<input
type="text"
[(ngModel)]="newOrg.displayName"
name="displayName"
class="input"
placeholder="My Organization"
placeholder="Defaults to name if empty"
/>
</div>
<div>
@@ -139,6 +139,11 @@ import { ToastService } from '../../core/services/toast.service';
class="w-4 h-4 border-border text-primary focus:ring-primary"
/>
<label for="isPublic" class="font-mono text-sm text-foreground">Make this organization public</label>
<button type="button" (click)="showPublicExplainer.set(true)" class="btn-ghost p-0 h-5 w-5 text-muted-foreground hover:text-foreground">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
</form>
<div class="card-footer flex justify-end gap-3">
@@ -154,6 +159,52 @@ import { ToastService } from '../../core/services/toast.service';
</div>
</div>
}
<!-- Public/Private Explainer Modal -->
@if (showPublicExplainer()) {
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 modal-backdrop">
<div class="card w-full max-w-md mx-4 modal-content">
<div class="card-header flex items-center justify-between">
<div class="section-header">
<div class="section-indicator"></div>
<span class="font-mono text-sm font-semibold text-foreground uppercase">Organization Visibility</span>
</div>
<button (click)="showPublicExplainer.set(false)" class="btn-ghost btn-sm p-1">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="card-content space-y-4">
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-accent/10 flex items-center justify-center flex-shrink-0">
<svg class="w-4 h-4 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<h4 class="font-mono text-sm font-semibold text-foreground mb-1">Public Organization</h4>
<p class="font-mono text-sm text-muted-foreground">Anyone can view this organization and its public repositories. Useful for open-source projects or public packages.</p>
</div>
</div>
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-primary/10 flex items-center justify-center flex-shrink-0">
<svg class="w-4 h-4 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<div>
<h4 class="font-mono text-sm font-semibold text-foreground mb-1">Private Organization</h4>
<p class="font-mono text-sm text-muted-foreground">Only organization members can see this organization and access its repositories. Best for internal or proprietary packages.</p>
</div>
</div>
</div>
<div class="card-footer flex justify-end">
<button (click)="showPublicExplainer.set(false)" class="btn-primary btn-md">Got it</button>
</div>
</div>
</div>
}
</div>
`,
})
@@ -164,6 +215,7 @@ export class OrganizationsComponent implements OnInit {
organizations = signal<IOrganization[]>([]);
loading = signal(true);
showCreateModal = signal(false);
showPublicExplainer = signal(false);
creating = signal(false);
newOrg = {

View File

@@ -102,8 +102,8 @@ interface IScopeEntry {
<!-- Create Modal -->
@if (showCreateModal()) {
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 overflow-y-auto py-8">
<div class="card w-full max-w-2xl mx-4">
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 overflow-y-auto py-8 modal-backdrop">
<div class="card w-full max-w-2xl mx-4 modal-content">
<div class="card-header flex items-center justify-between">
<div class="section-header">
<div class="section-indicator"></div>
@@ -284,8 +284,8 @@ interface IScopeEntry {
<!-- Token Created Modal -->
@if (createdToken()) {
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
<div class="card w-full max-w-lg mx-4">
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 modal-backdrop">
<div class="card w-full max-w-lg mx-4 modal-content">
<div class="card-header">
<div class="section-header">
<div class="section-indicator bg-accent"></div>

View File

@@ -259,4 +259,44 @@
.status-error {
@apply bg-destructive;
}
/* Modal animations */
.modal-backdrop {
@apply animate-fade-in;
}
.modal-content {
@apply animate-modal-in;
}
}
/* Custom animations */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes modal-in {
from {
opacity: 0;
transform: scale(0.95) translateY(-10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@layer utilities {
.animate-fade-in {
animation: fade-in 0.2s ease-out;
}
.animate-modal-in {
animation: modal-in 0.2s ease-out;
}
}