Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 81 additions & 9 deletions .cursor/rules/commits.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,51 @@ macro: update components

Before making any commit, you MUST complete ALL of the following steps:

> ⚠️ **CRITICAL REMINDER**: Steps 1 and 6 are often forgotten but are MANDATORY!

### 1. ✅ Remove Unnecessary Comments ⚠️ CRITICAL STEP
> ⚠️ **CRITICAL REMINDER**: Steps 1, 2, and 7 are often forgotten but are MANDATORY!

### 1. ✅ Check Git Branch and Create Feature Branch ⚠️ CRITICAL STEP
- **MANDATORY**: Check current branch with `git branch` or `git status`
- **If on main branch**: Create new feature branch before committing
- **Commands to execute**:
```bash
# Check current branch
git branch

# If on main, create feature branch
git checkout -b feature/[descriptive-name]

# Examples:
# git checkout -b feature/pwa-update-fixes
# git checkout -b feature/feature-workout-diary
# git checkout -b feature/auth-service-improvements
```
- **Branch naming convention**: `feature/[kebab-case-description]`
- **If working from a plan**: Use plan name as branch name
- **Example**: If plan is `feature-workout-diary.md`, branch should be `feature/feature-workout-diary`
- **Example**: If plan is `auth-service-improvements.md`, branch should be `feature/auth-service-improvements`
- **Plan file location**: `docs/plans/[plan-name].md`
- **Update plan status** as you progress through implementation
- **NEVER commit directly to main branch**
- **After creating feature branch**: Continue with steps 2-8
- **This step is CRITICAL for proper Git workflow!**

### 2. ✅ Remove Unnecessary Comments ⚠️ CRITICAL STEP
- **MANDATORY**: Delete ALL unnecessary comments from code before commit
- Use `grep -r "^\s*//" src/` to find all comments in changed files
- Keep only essential documentation comments
- **This step is often forgotten but is required!**

### 2. ✅ Fix Linter Errors
### 3. ✅ Fix Linter Errors
- Run `npm run lint` and fix all errors
- Ensure all files pass linting rules
- Fix TypeScript, Angular, and style linting issues

### 3. ✅ Verify Application Build
### 4. ✅ Verify Application Build
- Run `npm run build` and ensure successful compilation
- Fix any build errors or warnings
- Verify all imports and dependencies are correct

### 4. ✅ Check Test Coverage
### 5. ✅ Check Test Coverage
- Run `npm run test:ci` to check test coverage
- Ensure minimum coverage requirements are met:
- 80% statements
Expand All @@ -72,21 +98,67 @@ Before making any commit, you MUST complete ALL of the following steps:
- 80% lines
- Add tests for new functionality if coverage is insufficient

### 5. ✅ Verify All Tests Pass
### 6. ✅ Verify All Tests Pass
- All tests must pass in headless mode
- No tests should be skipped or failing
- Fix any broken tests before committing

### 6. ✅ Update Project Structure Documentation ⚠️ CRITICAL STEP
### 7. ✅ Update Project Structure Documentation ⚠️ CRITICAL STEP
- **MANDATORY**: Update `.cursor/rules/project-structure.mdc` if project structure changed
- Add new components, services, or features to documentation
- Keep architecture documentation current
- **This step is often forgotten but is required!**

### 8. ✅ Update PWA Version ⚠️ CRITICAL STEP
- **MANDATORY**: Update version in `ngsw-config.json` ONLY when application code changes
- **DO NOT update version** for test-only changes, documentation updates, or configuration changes
- Increment the version number in `appData.version` field only for functional changes
- This ensures PWA updates are detected by users only when needed
- **Example**: Change `"version": "1.0.4"` to `"version": "1.0.5"` only for app code changes
- **This step is critical for PWA update functionality!**

## Important Notes

- **Never commit with failing tests** - even if tests are unrelated to your changes
- **Always run tests in headless mode** - use `npm run test:ci`
- **Commit messages must be in English** and concise (max 3 lines)
- **Include branch name** at the beginning of commit message
- **Do not include test results** in commit message description
- **Do not include test results** in commit message description
- **Update PWA version ONLY for application code changes** in `ngsw-config.json` to ensure updates work when needed

## After Committing to Feature Branch

### Push Feature Branch
```bash
# Push feature branch to remote
git push -u origin feature/[branch-name]

# Example: git push -u origin feature/pwa-update-fixes
```

### Create Pull Request
- Create Pull Request from feature branch to main
- Add descriptive title and description
- Request code review if needed
- Merge after approval

### Clean Up After Merge
```bash
# Switch back to main
git checkout main

# Pull latest changes
git pull origin main

# Delete local feature branch
git branch -d feature/[branch-name]

# Delete remote feature branch (if needed)
git push origin --delete feature/[branch-name]
```

### Plan Management
- **If working from a plan**: Update plan status as you progress
- **Mark completed tasks** in the plan file
- **After feature completion**: Consider archiving or updating the plan
- **Plan location**: `docs/plans/[plan-name].md`
49 changes: 0 additions & 49 deletions .github/copilot-instructions.md

This file was deleted.

2 changes: 1 addition & 1 deletion ngsw-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@
}
],
"appData": {
"version": "1.0.4"
"version": "1.0.6"
}
}
29 changes: 24 additions & 5 deletions src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,36 @@ import { AppComponent } from './app.component';
class MockTuiRootComponent {}

describe('AppComponent', () => {
let telegramServiceSpy: jasmine.SpyObj<TelegramService>;
let themeServiceSpy: jasmine.SpyObj<ThemeService>;
let swUpdateServiceSpy: jasmine.SpyObj<SwUpdateService>;

beforeEach(async () => {
const mockWebApp = {
ready: jasmine.createSpy('ready'),
close: jasmine.createSpy('close'),
expand: jasmine.createSpy('expand'),
};

const telegramServiceSpy = jasmine.createSpyObj('TelegramService', [], {
telegramServiceSpy = jasmine.createSpyObj('TelegramService', [], {
webApp: mockWebApp,
});

const themeServiceSpy = jasmine.createSpyObj('ThemeService', ['toggleTheme', 'isDark'], {
isDark: jasmine.createSpy('isDark').and.returnValue(false),
});
themeServiceSpy = jasmine.createSpyObj(
'ThemeService',
['toggleTheme', 'isDark', 'initialize'],
{
isDark: jasmine.createSpy('isDark').and.returnValue(false),
},
);

const authServiceSpy = jasmine.createSpyObj('AuthService', ['initFromStorage'], {
isAuthenticated: jasmine.createSpy('isAuthenticated').and.returnValue(false),
loading: jasmine.createSpy('loading').and.returnValue(false),
error: jasmine.createSpy('error').and.returnValue(null),
});

const swUpdateServiceSpy = jasmine.createSpyObj('SwUpdateService', ['checkForUpdate']);
swUpdateServiceSpy = jasmine.createSpyObj('SwUpdateService', ['checkForUpdate', 'init']);

const userStoreSpy = jasmine.createSpyObj('UserStoreService', ['clearUser'], {
user: jasmine.createSpy().and.returnValue(null),
Expand Down Expand Up @@ -67,4 +75,15 @@ describe('AppComponent', () => {
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});

it('should initialize all services on ngOnInit', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;

app.ngOnInit();

expect(telegramServiceSpy.webApp.ready).toHaveBeenCalled();
expect(themeServiceSpy.initialize).toHaveBeenCalled();
expect(swUpdateServiceSpy.init).toHaveBeenCalled();
});
});
4 changes: 3 additions & 1 deletion src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { TuiRoot } from '@taiga-ui/core';

import { TelegramService, ThemeService } from '@/shared';
import { TelegramService, ThemeService, SwUpdateService } from '@/shared';
import { NavigationComponent } from '@/widgets';
import type { OnInit } from '@angular/core';

Expand All @@ -16,9 +16,11 @@ import type { OnInit } from '@angular/core';
export class AppComponent implements OnInit {
private readonly telegramService = inject(TelegramService);
private readonly themeService = inject(ThemeService);
private readonly swUpdateService = inject(SwUpdateService);

ngOnInit(): void {
this.telegramService.webApp.ready();
this.themeService.initialize();
this.swUpdateService.init();
}
}
36 changes: 36 additions & 0 deletions src/app/interceptors/auth.interceptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { of } from 'rxjs';
import { env } from '@/environments/env';
import { AuthService } from '@/features/auth';
import { configureZonelessTestingModule } from '@/test-setup';
import { authInterceptor } from './auth.interceptor';
import { TokenRefreshManager } from './token-refresh-manager';

describe('authInterceptor', () => {
let http: HttpClient;
Expand Down Expand Up @@ -72,4 +74,38 @@ describe('authInterceptor', () => {
const req = httpMock.expectOne('/api/protected');
req.flush(null, { status: 500, statusText: 'Internal Server Error' });
});

it('should navigate to login on 401 error when refresh fails', (done) => {
const router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
authService.getAccessToken.and.returnValue('test-token');
authService.refreshToken$.and.returnValue(of(false));

http.get('/api/protected').subscribe({
next: () => {},
error: () => {
expect(router.navigate).toHaveBeenCalledWith(['/login']);
done();
},
});

const req = httpMock.expectOne('/api/protected');
req.flush(null, { status: 401, statusText: 'Unauthorized' });
});

it('should queue request when refresh is already in progress', () => {
const refreshManager = TokenRefreshManager.getInstance();
const addPendingRequestSpy = spyOn(refreshManager, 'addPendingRequest').and.callThrough();

authService.getAccessToken.and.returnValue('test-token');
authService.refreshToken$.and.returnValue(of(true));

refreshManager.setRefreshInProgress(true);

http.get('/api/queued').subscribe();

const req = httpMock.expectOne('/api/queued');
req.flush(null, { status: 401, statusText: 'Unauthorized' });

expect(addPendingRequestSpy).toHaveBeenCalledWith(jasmine.any(Function));
});
});
8 changes: 4 additions & 4 deletions src/app/interceptors/auth.interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, switchMap, throwError, defer, finalize } from 'rxjs';
import { catchError, switchMap, throwError, defer, finalize, Observable } from 'rxjs';
import { AuthService } from '@/features/auth';
import { TokenRefreshManager } from './token-refresh-manager';

Expand All @@ -11,7 +11,6 @@ import type {
HttpErrorResponse,
HttpEvent,
} from '@angular/common/http';
import type { Observable } from 'rxjs';

const AUTH_ENDPOINTS = ['/v1/auth/login', '/v1/auth/register', '/v1/auth/refresh'] as const;
const shouldSkipAuth = (req: HttpRequest<unknown>): boolean => {
Expand All @@ -32,9 +31,10 @@ const handle401Error = (

if (refreshManager.isRefreshInProgress) {
return defer(() => {
return new Promise<Observable<HttpEvent<unknown>>>((resolve) => {
return new Observable<Observable<HttpEvent<unknown>>>((subscriber) => {
refreshManager.addPendingRequest(() => {
resolve(next(req));
subscriber.next(next(req));
subscriber.complete();
});
});
}).pipe(switchMap((observable) => observable));
Expand Down
16 changes: 16 additions & 0 deletions src/shared/services/sw-update/sw-update.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,20 @@ describe('SwUpdateService', () => {
expect(service).toBeTruthy();
expect(swUpdateSpy.isEnabled).toBe(true);
});

it('should initialize service', () => {
service = TestBed.inject(SwUpdateService);
expect(() => service.init()).not.toThrow();
});

it('should handle init when service worker is disabled', () => {
const disabledSwUpdateSpy = jasmine.createSpyObj('SwUpdate', ['checkForUpdate'], {
isEnabled: false,
versionUpdates: versionUpdatesSubject.asObservable(),
});

TestBed.overrideProvider(SwUpdate, { useValue: disabledSwUpdateSpy });
service = TestBed.inject(SwUpdateService);
expect(() => service.init()).not.toThrow();
});
});
2 changes: 1 addition & 1 deletion src/shared/services/sw-update/sw-update.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class SwUpdateService {
private readonly swUpdate = inject(SwUpdate);
private readonly destroyRef = inject(DestroyRef);

constructor() {
init(): void {
if (this.swUpdate.isEnabled) {
this.initializeUpdateChecks();
this.handleUpdates();
Expand Down
Loading