Compare commits

..
13 changed files with 589 additions and 561 deletions
+1 -2
View File
@@ -53,7 +53,6 @@
"@types/geojson": "1.0.6",
"@types/node": "22.15.31",
"@types/node-cron": "3.0.7",
"@types/nodemailer": "8.0.2",
"@types/promise-queue": "2.2.0",
"@types/uuid": "8.3.4",
"body-parser": "1.20.3",
@@ -69,7 +68,7 @@
"nock": "13.3.1",
"node-cache": "5.1.2",
"node-cron": "3.0.2",
"nodemailer": "8.0.11",
"nodemailer": "10.1.7",
"prom-client": "14.1.1",
"promise-queue": "2.2.5",
"uuid": "8.3.2"
+6 -6
View File
@@ -48,13 +48,13 @@
"test:integration": "sh integration-test.sh"
},
"dependencies": {
"@angular/animations": "20.3.31",
"@angular/animations": "20.3.32",
"@angular/cdk": "20.2.14",
"@angular/common": "20.3.31",
"@angular/core": "20.3.31",
"@angular/forms": "20.3.31",
"@angular/platform-browser": "20.3.31",
"@angular/router": "20.3.31",
"@angular/common": "20.3.32",
"@angular/core": "20.3.32",
"@angular/forms": "20.3.32",
"@angular/platform-browser": "20.3.32",
"@angular/router": "20.3.32",
"@awesome-cordova-plugins/calendar": "8.1.0",
"@awesome-cordova-plugins/core": "8.1.0",
"@capacitor-community/in-app-review": "8.0.0",
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2026 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {TestBed} from '@angular/core/testing';
import {HttpClient} from '@angular/common/http';
import {BehaviorSubject, of} from 'rxjs';
import {SCAssessment} from '@openstapps/core';
import {AssessmentsProvider} from './assessments.provider';
import {ConfigProvider} from '../config/config.provider';
import {AuthActions, AuthProvider, IAuthAction} from '../auth/auth.provider';
class FakeAuth {
events$ = new BehaviorSubject<IAuthAction>({action: AuthActions.Init});
accessToken = 'token-a';
getValidToken() {
return Promise.resolve({accessToken: this.accessToken});
}
}
const gradesA = [{uid: 'a', name: 'Grade of A'}] as unknown as SCAssessment[];
const gradesB = [{uid: 'b', name: 'Grade of B'}] as unknown as SCAssessment[];
describe('AssessmentsProvider', () => {
let provider: AssessmentsProvider;
let fakeAuth: FakeAuth;
let httpClient: jasmine.SpyObj<HttpClient>;
beforeEach(() => {
fakeAuth = new FakeAuth();
httpClient = jasmine.createSpyObj<HttpClient>('HttpClient', ['get']);
httpClient.get.and.callFake(((_url: string, options: {headers: {Authorization: string}}) =>
of({data: options.headers.Authorization === 'Bearer token-a' ? gradesA : gradesB})) as never);
TestBed.configureTestingModule({
providers: [
AssessmentsProvider,
{
provide: ConfigProvider,
useValue: {config: {app: {features: {extern: {hisometry: {url: 'http://hisometry.local'}}}}}},
},
{provide: AuthProvider, useValue: fakeAuth},
{provide: HttpClient, useValue: httpClient},
],
});
provider = TestBed.inject(AssessmentsProvider);
});
it('should use the cache while the same user is logged in', async () => {
await provider.getAssessments();
fakeAuth.events$.next({action: AuthActions.RefreshSuccess});
expect(await provider.getAssessments()).toEqual(gradesA);
expect(httpClient.get).toHaveBeenCalledTimes(1);
});
for (const action of [AuthActions.SignOutSuccess, AuthActions.SessionExpired]) {
it(`should clear the cache on ${action}`, async () => {
await provider.getAssessments();
fakeAuth.events$.next({action});
expect(provider.cache).toBeUndefined();
expect(await provider.assessments).toEqual({});
});
}
it('should not show the grades of the previous user after a user change', async () => {
expect(await provider.getAssessments()).toEqual(gradesA);
fakeAuth.events$.next({action: AuthActions.SignOutSuccess});
fakeAuth.accessToken = 'token-b';
fakeAuth.events$.next({action: AuthActions.SignInSuccess});
expect(await provider.getAssessments()).toEqual(gradesB);
expect(httpClient.get).toHaveBeenCalledTimes(2);
});
});
@@ -17,7 +17,7 @@ import {ConfigProvider} from '../config/config.provider';
import {SCAssessment, SCUuid} from '@openstapps/core';
import {HttpClient} from '@angular/common/http';
import {uniqBy, keyBy} from '@openstapps/collection-utils';
import {AuthProvider} from '../auth/auth.provider';
import {AuthActions, AuthProvider} from '../auth/auth.provider';
/**
*
@@ -57,6 +57,24 @@ export class AssessmentsProvider {
readonly http = inject(HttpClient);
constructor() {
/*
* The grades belong to the logged-in user. The next user
* should never get the previous user's grades from the cache.
*/
this.authProvider.events$.subscribe(({action}) => {
switch (action) {
case AuthActions.SignInSuccess:
case AuthActions.SignOutSuccess:
case AuthActions.SignOutFailed:
case AuthActions.SessionExpired: {
this.clearCache();
break;
}
}
});
}
assessmentPath = 'assessments';
// usually this wouldn't be necessary, but the assessment service
@@ -110,4 +128,13 @@ export class AssessmentsProvider {
return this.cache;
}
/**
* Removes all cached grades.
*/
clearCache(): void {
this.cache = undefined;
this.assessments = Promise.resolve({});
this.cacheTimestamp = 0;
}
}
@@ -23,6 +23,7 @@ import {DataProvider, DataScope} from '../../data/data.provider';
import {DataRoutingService} from '../../data/data-routing.service';
import {groupBy, mapValues} from '@openstapps/collection-utils';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {environment} from '../../../../environments/environment';
@Component({
selector: 'app-assessments-page',
@@ -62,6 +63,26 @@ export class AssessmentsPageComponent implements OnInit, AfterViewInit {
destroy$ = inject(DestroyRef);
/**
* ?token=... (e.g. ?token=mock) is a development aid only.
* In production, no token is read from or passed on in URLs.
*/
private readonly urlTokenAllowed = !environment.production;
/**
* Token from the URL (?token=...), development only.
* Always undefined in production, so no token is read from or passed on in URLs.
*/
get urlToken(): string | undefined {
if (!this.urlTokenAllowed) {
return;
}
const token = this.activatedRoute.snapshot.queryParamMap.get('token');
return token === null ? undefined : token;
}
ngAfterViewInit() {
this.segmentView.value = this.sharedAxisChoreographer.currentValue;
}
@@ -73,7 +94,7 @@ export class AssessmentsPageComponent implements OnInit, AfterViewInit {
.subscribe(thing => {
void this.router.navigate(['assessments', 'detail', thing.uid], {
queryParams: {
token: this.activatedRoute.snapshot.queryParamMap.get('token'),
token: this.urlToken,
},
state: {item: thing},
});
@@ -82,7 +103,7 @@ export class AssessmentsPageComponent implements OnInit, AfterViewInit {
this.activatedRoute.queryParams.subscribe(parameters => {
try {
this.assessments = this.assessmentsProvider
.getAssessments(parameters.token)
.getAssessments(this.urlTokenAllowed ? parameters.token : undefined)
.then(assessments => groupBy(assessments, it => it.courseOfStudy?.uid ?? 'unknown'))
.then(it => {
this.assessmentKeys = Object.keys(it);
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -51,7 +51,7 @@
"commander": "10.0.0",
"fs-extra": "10.1.0",
"junit-report-builder": "3.0.1",
"wait-on": "6.0.1"
"wait-on": "9.5.1"
},
"devDependencies": {
"@openstapps/eslint-config": "workspace:*",
+4 -4
View File
@@ -52,8 +52,8 @@
"better-ajv-errors": "1.2.0",
"commander": "10.0.0",
"deepmerge": "4.3.1",
"del": "6.1.1",
"flatted": "3.2.7",
"del": "9.0.1",
"flatted": "3.4.4",
"fs-extra": "10.1.0",
"glob": "10.5.0",
"got": "12.6.0",
@@ -62,9 +62,9 @@
"mustache": "4.2.0",
"openapi-types": "12.1.0",
"plantuml-encoder": "1.4.0",
"re2": "1.21.0",
"re2": "1.27.0",
"toposort": "2.0.2",
"ts-json-schema-generator": "1.2.0"
"ts-json-schema-generator": "1.5.0"
},
"devDependencies": {
"@openstapps/eslint-config": "workspace:*",
+1 -1
View File
@@ -1,5 +1,5 @@
import {defineConfig} from 'tsup';
import packageJson from './package.json' assert {type: 'json'};
import packageJson from './package.json' with {type: 'json'};
export default defineConfig({
entry: ['src/index.ts'],
+1 -1
View File
@@ -29,7 +29,7 @@
"@elastic/elasticsearch": "8.14.1",
"commander": "10.0.0",
"deepmerge": "4.3.1",
"flatted": "3.2.7",
"flatted": "3.4.4",
"typedoc": "0.18.0",
"typescript": "3.8.3"
},
+2 -3
View File
@@ -28,10 +28,9 @@
"test": "c8 mocha"
},
"dependencies": {
"@types/nodemailer": "8.0.2",
"chalk": "5.2.0",
"flatted": "3.2.7",
"nodemailer": "8.0.11"
"flatted": "3.4.4",
"nodemailer": "10.1.7"
},
"devDependencies": {
"@openstapps/eslint-config": "workspace:*",
+4 -4
View File
@@ -312,19 +312,19 @@ export class SMTP extends VerifiableTransport {
*/
public async sendMail(mail: SendMailOptions): Promise<string> {
// info is the response of the smtp server
let info = await this.transportAgent.sendMail(mail);
const info = await this.transportAgent.sendMail(mail);
// it can be undefined (empty response)
if (info === undefined) {
info = 'Successfully sent mail';
(info['message'] as string) = 'Successfully sent mail';
}
// or not of type string
if (typeof info !== 'string') {
info = JSON.stringify(info);
(info.message as string) = JSON.stringify(info.message);
}
return info;
return info.message;
}
/**
+358 -452
View File
File diff suppressed because it is too large Load Diff