Skip to content

Commit

Permalink
Merge branch 'master' into remove-my-favorites
Browse files Browse the repository at this point in the history
  • Loading branch information
llorentelemmc authored Sep 3, 2024
2 parents 2567f5b + 94c9568 commit 6238f7c
Show file tree
Hide file tree
Showing 17 changed files with 296 additions and 160 deletions.
1 change: 1 addition & 0 deletions AMW_angular/io/.husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
git-format-staged -f 'prettier --ignore-unknown --stdin --stdin-filepath "{}"' .
8 changes: 8 additions & 0 deletions AMW_angular/io/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ npm run backend:start
npm run backend:stop
```

## Coding guidelines

See [coding-guideline.md](./coding-guidelines.md)

## Code Formatting

We use Prettier for code-formatting. Additionally, there is a pre-commit hook to format all staged files with husky. [More info](https://github.com/hallettj/git-format-staged)

## Code scaffolding

Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
Expand Down
11 changes: 2 additions & 9 deletions AMW_angular/io/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@
"base": "dist"
},
"index": "src/index.html",
"polyfills": [
"zone.js",
"@angular/localize/init"
],
"polyfills": ["zone.js", "@angular/localize/init"],
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
Expand Down Expand Up @@ -99,11 +96,7 @@
"builder": "@angular-devkit/build-angular:web-test-runner",
"options": {
"main": "src/test.ts",
"polyfills": [
"zone.js",
"zone.js/testing",
"@angular/localize/init"
],
"polyfills": ["zone.js", "zone.js/testing", "@angular/localize/init"],
"tsConfig": "tsconfig.spec.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["node_modules/bootstrap/dist/css/bootstrap.min.css", "src/styles.scss"]
Expand Down
76 changes: 76 additions & 0 deletions AMW_angular/io/coding-guidelines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Coding Guidelines


## Inject() over Constructor-Injections

Use `inject()` instead of constuctor-injection to make the code more explicit and obvious.

```typescript
// use
myService = inject(MyService);

// instead of
constructor(
private myservice: MyService
) {}
```

## RxJS
Leverage RxJS for API calls, web sockets, and complex data flows, especially when handling multiple asynchronous events. Combine with Signals to simplify component state management.

## Signals

Use signals for changing values in Components

### Signal from Observable

#### GET

Make API-requests with observables and expose the state as a signal:

```typescript
// retrive data from API using RxJS
private users$ = this.http.get<User[]>(this.userUrl);

// expose as signal
users = toSignal(this.users$, { initialValue: [] as User[]});
```

The observable `users$` is just used to pass the state to the _readonly_ signal `users`. (The signals created from an observable are always _readonly_!)
No need for unsubscription - this is handled by `toSignal()` automatically.

Use the signal in the component not in the template. (Separation of concerns)

#### POST / DELETE etc.

To update data in a signal you have to create a WritableSignal:

```typescript
// WritableSignal
users = signal<Tag[]>([]);
// retrive data from API using RxJS and write it in the WritableSignal
private users$ = this.http.get<User[]>(this.userUrl).pipe(tap((users) => this.users.set(users)));
// only used to automatically un-/subscribe to the observable
readOnlyUsers = toSignal(this.users$, { initialValue: [] as User[]});
```

## Auth Service

The frontend provides a singelton auth-service which holds all restrictions for the current user.

After injecting the service in your component you can get Permissions/Actions depending on your needs:

```typescript
// inject the service
authService = inject(AuthService);

// get actions for a specific permission
const actions = this.authService.getActionsForPermission('MY_PERMISSION');

// verify role in an action and set signal
this.canCreate.set(actions.some(isAllowed('CREATE')));

// or directly set signal based on a concret permission and action value
this.canViewSettings.set(this.authService.hasPermission('SETTINGS', 'READ'));

```
28 changes: 28 additions & 0 deletions AMW_angular/io/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions AMW_angular/io/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
"backend:stop": "docker compose -f ./../../AMW_docker/docker-compose/docker-compose.yml down",
"build": "ng build",
"test": "ng test",
"test-headless": "ng test --browsers=ChromeHeadless",
"test:watch": "ng test --watch",
"lint": "ng lint",
"prettier": "npx prettier --write .",
"mavenbuild": "ng test --watch=false --browsers=ChromeHeadless && ng build --configuration production"
"mavenbuild": "ng test --watch=false --browsers=ChromeHeadless && ng build --configuration production",
"prepare": "husky"
},
"private": true,
"dependencies": {
Expand Down Expand Up @@ -56,6 +57,8 @@
"eslint": "8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.0.1",
"git-format-staged": "^3.1.1",
"husky": "^9.1.5",
"jasmine-core": "~5.1.1",
"jasmine-spec-reporter": "~7.0.0",
"prettier": "3.0.3",
Expand Down
93 changes: 60 additions & 33 deletions AMW_angular/io/src/app/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,109 @@
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { AuthService } from './auth.service';
import { provideHttpClient } from '@angular/common/http';

describe('AuthService', () => {
let authService: AuthService;
let httpTestingController: HttpTestingController;
let API_URL: string;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [AuthService],
imports: [],
providers: [AuthService, provideHttpClient(), provideHttpClientTesting()],
});
authService = TestBed.inject(AuthService);
httpTestingController = TestBed.inject(HttpTestingController);
API_URL = `${authService.getBaseUrl()}/permissions/restrictions/ownRestrictions/`;
});

afterEach(() => {
httpTestingController.verify();
});

it('should return true if the user has the specified permission and action', () => {
it('should reload permissions', () => {
authService.refreshData();
const requests = httpTestingController.match(API_URL);
expect(requests.length).toEqual(2);
});

it('should return actions for a permission', () => {
const permissionName = 'examplePermission';
const CREATE = 'CREATE';
const READ = 'READ';

const req = httpTestingController.expectOne(API_URL);
expect(req.request.method).toBe('GET');
req.flush([
{ permission: { name: permissionName }, action: READ },
{ permission: { name: permissionName }, action: CREATE },
{ permission: { name: 'secondPermission' }, action: CREATE },
{ permission: { name: 'thirdPermission' }, action: CREATE },
]);

expect(authService.getActionsForPermission(permissionName)).toEqual([READ, CREATE]);
});

it('should not return actions for a missing permission', () => {
const permissionName = 'examplePermission';
const action = 'CREATE';

authService.hasPermission(permissionName, action).subscribe((result) => {
expect(result).toBeTrue();
});
const req = httpTestingController.expectOne(API_URL);
expect(req.request.method).toBe('GET');
req.flush([
{ permission: { name: 'firstPermiison' }, action: action },
{ permission: { name: 'secondPermission' }, action: action },
{ permission: { name: 'thirdPermission' }, action: action },
]);

const req = httpTestingController.expectOne(`${authService.getBaseUrl()}/permissions/restrictions/ownRestrictions/`);
expect(authService.getActionsForPermission(permissionName)).toEqual([]);
});

it('should return true if the user has the specified permission and action', () => {
const permissionName = 'examplePermission';
const action = 'CREATE';

const req = httpTestingController.expectOne(API_URL);
expect(req.request.method).toBe('GET');
req.flush([
{ permission: { name: permissionName }, action: 'READ' },
{ permission: { name: permissionName }, action: action },
]);

expect(authService.hasPermission(permissionName, action)).toBeTrue();
});

it('should return true if the user has the specified permission and ALL action', () => {
const permissionName = 'examplePermission';
const action = 'CREATE';

authService.hasPermission(permissionName, action).subscribe((result) => {
expect(result).toBeTrue();
});

const req = httpTestingController.expectOne(`${authService.getBaseUrl()}/permissions/restrictions/ownRestrictions/`);
const req = httpTestingController.expectOne(API_URL);
expect(req.request.method).toBe('GET');
req.flush([
{ permission: { name: permissionName }, action: 'ALL' }
]);
req.flush([{ permission: { name: permissionName }, action: 'ALL' }]);

expect(authService.hasPermission(permissionName, action)).toBeTrue();
});

it('should return false if the user has the specified permission and but not action', () => {
const permissionName = 'examplePermission';
const action = 'CREATE';

authService.hasPermission(permissionName, action).subscribe((result) => {
expect(result).toBeFalse();
});

const req = httpTestingController.expectOne(`${authService.getBaseUrl()}/permissions/restrictions/ownRestrictions/`);
const req = httpTestingController.expectOne(API_URL);
expect(req.request.method).toBe('GET');
req.flush([
{ permission: { name: permissionName }, action: 'READ' }
]);
req.flush([{ permission: { name: permissionName }, action: 'READ' }]);

expect(authService.hasPermission(permissionName, action)).toBeFalse();
});

it("should return false if the user doesn't have the specified permission", () => {
const permissionName = 'examplePermission';
const action = 'CREATE';

authService.hasPermission(permissionName, action).subscribe((result) => {
expect(result).toBeFalse();
});

const req = httpTestingController.expectOne(`${authService.getBaseUrl()}/permissions/restrictions/ownRestrictions/`);
const req = httpTestingController.expectOne(API_URL);
expect(req.request.method).toBe('GET');
req.flush([
{ permission: { name: "otherPermission" }, action: 'READ' }
]);
req.flush([{ permission: { name: 'otherPermission' }, action: 'READ' }]);

expect(authService.hasPermission(permissionName, action)).toBeFalse();
});
});
});
Loading

0 comments on commit 6238f7c

Please sign in to comment.