Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
0aeb3d8
feat(backend): add a new property containing the url the help button …
fstreit1 Jul 7, 2026
b737551
feat(backend): write a configuration method for getting the support p…
fstreit1 Jul 7, 2026
5d5eba7
feat(backend): add a new dto for transfering the support page url #541
fstreit1 Jul 7, 2026
4cacdd2
feat(backend): write a mapper for translating the url string to a dto…
fstreit1 Jul 7, 2026
3f9ca7f
feat(backend): change the old enpoint url and add a new endpoint for …
fstreit1 Jul 7, 2026
608453b
feat(backend): change the security config so the new endpoint for get…
fstreit1 Jul 7, 2026
a72c94b
feat(frontend): add a new help attribute to the de.json #541
fstreit1 Jul 7, 2026
bc211cc
feat(frontend): change the test so the url points to the old endpoint…
fstreit1 Jul 7, 2026
bac56f4
feat(frontend): add a fetch for the new endpoint which provides the s…
fstreit1 Jul 7, 2026
7f47000
feat(frontend): implement a new model for containing the support page…
fstreit1 Jul 7, 2026
29d43f2
fix(frontend): rewrite import so modelname fits PascalCase convention…
fstreit1 Jul 7, 2026
b06cd45
feat(frontend): add a new signal for containing the href to the suppo…
fstreit1 Jul 7, 2026
b98df21
feat(frontend): add the button in the top bar #541
fstreit1 Jul 7, 2026
3b7c04c
refactor(backend): remove unnecessary default value #541
fstreit1 Jul 7, 2026
0e05cf3
feat(backend): add check if configured url is valid #541
fstreit1 Jul 7, 2026
07bf144
fix(backend): rewrite check for valid url so the url can not contain …
fstreit1 Jul 7, 2026
b2a8d16
test(backend): write tests for the check if a url is valid #541
fstreit1 Jul 7, 2026
a5f2b56
feat(backend): add check if the url is valid #541
fstreit1 Jul 7, 2026
ac2add8
fix(backend): fix test so it uses the changed endpoint url #541
fstreit1 Jul 7, 2026
a281790
test(backend): write test if the endpoint deliveres the correct url …
fstreit1 Jul 7, 2026
f1d8b55
test: write test data in properties file and in test-data #541
fstreit1 Jul 7, 2026
de804ca
test(frontend): write a test if the fetch for the support page endpoi…
fstreit1 Jul 7, 2026
4dc3947
fix(backend): fix the test which checks if the endpoint for providing…
fstreit1 Jul 7, 2026
62e97b9
test(frontend): write frontend test #541
fstreit1 Jul 8, 2026
1048e85
fix(backend): fix naming of configuration property #541
fstreit1 Jul 8, 2026
c30349e
fix(backend): fix property naming #541
fstreit1 Jul 8, 2026
b2cf876
test(frontend): add e2e test #541
fstreit1 Jul 8, 2026
cb99225
refactor(backend): rename helpurlconfiguration into appconfiguration …
fstreit1 Jul 8, 2026
471256f
refactor(backend): rename property to help-url #541
fstreit1 Jul 8, 2026
4a45bc6
refactor(backend): rename support mapper to appconfigurationmapper #541
fstreit1 Jul 8, 2026
beedaa8
fix(backend): fix naming #541
fstreit1 Jul 8, 2026
ae59696
fix(frontend): refactor the test so it uses the test id and not the n…
fstreit1 Jul 8, 2026
338f059
fix(frontend): remove id from the <a> element #541
fstreit1 Jul 8, 2026
f04c54b
test(backend): remove broken tests after refactoring the check for va…
fstreit1 Jul 8, 2026
f10b664
refactor(backend): remove check and add annotation @URL #541
fstreit1 Jul 8, 2026
03d91f2
fix(frontend): fix test for checking if endpoint works #541
fstreit1 Jul 8, 2026
7d1ae3a
test(backend): write new tests for @url annotation testing #541
fstreit1 Jul 8, 2026
7684b05
test(backend): write a test for annotation violations #541
fstreit1 Jul 8, 2026
bcc040e
fix(backend): rename properties #541
fstreit1 Jul 8, 2026
8d09661
refactor(backend): rename method to prevent clash with parameter #541
fstreit1 Jul 8, 2026
82f4608
test(frontend): refactor the test so it fits into the angular testing…
fstreit1 Jul 9, 2026
e5ab920
test(frontend): refactor the test so it uses convention from angular …
fstreit1 Jul 9, 2026
68a1a2b
refactor(backend): rename constant to fit regex #541
fstreit1 Jul 9, 2026
e6523bf
test(frontend): make test function async #541
fstreit1 Jul 9, 2026
8a72d89
test(frontend): fix test by making it intercept the request to the ba…
fstreit1 Jul 9, 2026
d6f7097
fix(frontend): rewrite service to use rxresource #541
fstreit1 Jul 9, 2026
5bd9191
feat(backend): add new check if its a valid url #541
fstreit1 Jul 10, 2026
980d8f3
test(frontend): refactor fetching from endpoint and test #541
fstreit1 Jul 10, 2026
59aeb5a
feat(backend): fix naming of properties #541
fstreit1 Jul 10, 2026
0a1a6de
fix(backend): refactor e2e test url to make it conform to the new rul…
fstreit1 Jul 10, 2026
a71dd9e
refactor(backend): rename dto #541
fstreit1 Jul 10, 2026
3be11aa
refactor(frontend): adjust fetch to fit new endpoint naming #541
fstreit1 Jul 10, 2026
6487296
fix(frontend): change fetch to match new endpoint naming #541
fstreit1 Jul 13, 2026
a260700
feat(backend): add new check if the url configured contains a space …
fstreit1 Jul 13, 2026
016842c
test(backend): refactor tests into one parameterized one #541
fstreit1 Jul 13, 2026
1ca339a
Merge branch 'main' into feature/541-add-help-button
fstreit1 Jul 13, 2026
117b49c
fix(frontend): change url to point to renamed endpoint #541
fstreit1 Jul 13, 2026
3dd10f8
refactor(backend): change displayname of test #541
fstreit1 Jul 13, 2026
c1a14e4
refactor(backend): make the checks more readable and fix the naming …
fstreit1 Jul 13, 2026
722eb8d
refactor(backend): rename the properties to help-url #541
fstreit1 Jul 13, 2026
55b4ce7
refactor(backend): renamed many things #541
fstreit1 Jul 13, 2026
3820b4d
test(backend): split test into negative cases and positives for reada…
fstreit1 Jul 13, 2026
16eb747
refactor(frontend): rename attributes #541
fstreit1 Jul 14, 2026
7822ecb
test(backend): fix configurationcontroller integration test #541
fstreit1 Jul 14, 2026
8fe935b
refactor(frontend): rename attribute #541
fstreit1 Jul 14, 2026
4f756e1
refactor(frontend): refactor structure #541
fstreit1 Jul 14, 2026
2adf6ae
test(frontend): fix flakey test #541
fstreit1 Jul 14, 2026
1fd9514
fix(backend): resolve issues from sonarqube #541
fstreit1 Jul 14, 2026
0217948
Merge branch 'main' into feature/541-add-help-button
fstreit1 Jul 14, 2026
b567287
test(frontend): add new test #541
fstreit1 Jul 14, 2026
1c810fd
Merge branch 'main' into feature/541-add-help-button
fstreit1 Jul 15, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ch.puzzle.pcts.configuration;

import jakarta.validation.constraints.NotBlank;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@Validated
@ConfigurationProperties(prefix = "pcts.app.help")
Comment thread
fstreit1 marked this conversation as resolved.
Outdated
public record HelpUrlConfiguration(@NotBlank String url) {
Comment thread
fstreit1 marked this conversation as resolved.
Outdated
public String helpUrl() {
Comment thread
fstreit1 marked this conversation as resolved.
Outdated
if (isValid()) {
return url;
} else {
throw new IllegalStateException("Invalid support url configurated");
}
}

private boolean isValid() {
return url.startsWith("https://") && url.length() > 8 && !url.contains(" ") && !url.substring(8).isBlank();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package ch.puzzle.pcts.controller;

import ch.puzzle.pcts.configuration.AuthorizationConfiguration;
import ch.puzzle.pcts.configuration.HelpUrlConfiguration;
import ch.puzzle.pcts.dto.configuration.ConfigurationDto;
import ch.puzzle.pcts.dto.support.SupportDto;
import ch.puzzle.pcts.mapper.ConfigurationMapper;
import ch.puzzle.pcts.mapper.SupportMapper;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -13,16 +16,26 @@
@RequestMapping("/api/v1/configuration")
@Tag(name = "configuration", description = "Retrieve configuration properties for the application")
public class ConfigurationController {
private final ConfigurationMapper mapper;
private final ConfigurationMapper configMapper;
Comment thread
fstreit1 marked this conversation as resolved.
Outdated
private final SupportMapper supportMapper;
Comment thread
fstreit1 marked this conversation as resolved.
Outdated
private final AuthorizationConfiguration authConfig;
private final HelpUrlConfiguration helpUrlConfig;

public ConfigurationController(ConfigurationMapper mapper, AuthorizationConfiguration authConfig) {
this.mapper = mapper;
public ConfigurationController(ConfigurationMapper configMapper, SupportMapper supportMapper,
AuthorizationConfiguration authConfig, HelpUrlConfiguration helpUrlConfig) {
this.configMapper = configMapper;
this.supportMapper = supportMapper;
this.authConfig = authConfig;
this.helpUrlConfig = helpUrlConfig;
}

@GetMapping
@GetMapping("/authorization")
public ResponseEntity<ConfigurationDto> getAuthorizationConfiguration() {
return ResponseEntity.ok(mapper.toDto(this.authConfig));
return ResponseEntity.ok(configMapper.toDto(this.authConfig));
}

@GetMapping("/help")
public ResponseEntity<SupportDto> getHelpUrl() {
return ResponseEntity.ok(supportMapper.toDto(this.helpUrlConfig.helpUrl()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ch.puzzle.pcts.dto.support;

import io.swagger.v3.oas.annotations.media.Schema;

public record SupportDto(@Schema(description = "Provides the url the help button should lead to") String url) {

}
11 changes: 11 additions & 0 deletions backend/src/main/java/ch/puzzle/pcts/mapper/SupportMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package ch.puzzle.pcts.mapper;

import ch.puzzle.pcts.dto.support.SupportDto;
import org.springframework.stereotype.Component;

@Component
public class SupportMapper {
public SupportDto toDto(String url) {
return new SupportDto(url);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) {
.permitAll()
.requestMatchers("/actuator/health/**")
.permitAll()
.requestMatchers("/api/v1/configuration")
.requestMatchers("/api/v1/configuration/**")
.permitAll()
.anyRequest()
.authenticated())
Expand Down
2 changes: 2 additions & 0 deletions backend/src/main/resources/application-dev.properties
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ app.member-sync.password=
app.member-sync.enabled=false
app.member-sync.cron=0 * * * * ?

pcts.app.help.url=https://google.com

# OAuth
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8544/realms/pitc
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8544/realms/pitc/protocol/openid-connect/certs
Expand Down
2 changes: 2 additions & 0 deletions backend/src/main/resources/application-e2e-test.properties
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ app.member-sync.cron=0 0 3 * * ?
app.member-sync.username=
app.member-sync.password=

pcts.app.help.url=https://dummy-url.test

# Authentication & JWT
pcts.security.authentication.display-name-claim=name
pcts.security.authorization.admin-authorities=org_hr,org_gl
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package ch.puzzle.pcts.configuration;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class HelpUrlConfigurationTest {
@DisplayName("Should throw exception when url does not start with https://")
@Test
void shouldThrowExceptionWhenUrlDoesNotStartWithHttps() {
String url = "http://example.com";

var config = new HelpUrlConfiguration(url);

assertThrows(IllegalStateException.class, () -> config.helpUrl());
}

@DisplayName("Should throw exception when url ends after https:// so when length is not over 8")
@Test
void shouldThrowExceptionWhenUrlIsNotLongerThan8Chars() {
String url = "https://";

var config = new HelpUrlConfiguration(url);

assertThrows(IllegalStateException.class, () -> config.helpUrl());
}

@DisplayName("Should throw exception when url contains a space")
@Test
void shouldThrowExceptionWhenUrlContainsSpace() {
String url = "https://in validUrl.com";

var config = new HelpUrlConfiguration(url);

assertThrows(IllegalStateException.class, () -> config.helpUrl());
}

@DisplayName("Should throw exception when url is blank after index 8")
@Test
void shouldThrowExceptionWhenUrlIsBlankAfterIndex8() {
String url = "https:// ";

var config = new HelpUrlConfiguration(url);

assertThrows(IllegalStateException.class, () -> config.helpUrl());
}

@DisplayName("Should return correct url a valid url is configured")
@Test
void shouldReturnCorrectUrlWhenUrlIsValid() {
String url = "https://example.com";

var config = new HelpUrlConfiguration(url);

assertEquals(url, config.helpUrl());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import ch.puzzle.pcts.configuration.AuthorizationConfiguration;
import ch.puzzle.pcts.configuration.HelpUrlConfiguration;
import ch.puzzle.pcts.dto.configuration.ConfigurationDto;
import ch.puzzle.pcts.dto.support.SupportDto;
import ch.puzzle.pcts.mapper.ConfigurationMapper;
import ch.puzzle.pcts.mapper.SupportMapper;
import ch.puzzle.pcts.util.JsonDtoMatcher;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -23,30 +26,52 @@ class ConfigurationControllerIT extends ControllerITBase {
@MockitoBean
private AuthorizationConfiguration authorizationConfiguration;

private static final String supportUrl = "https://example.com";

@MockitoBean
private HelpUrlConfiguration helpUrlConfiguration;

@MockitoBean
private ConfigurationMapper mapper;
private ConfigurationMapper configMapper;

@MockitoBean
private SupportMapper supportMapper;

@Autowired
private MockMvc mvc;

private static final String BASEURL = "/api/v1/configuration";

private ConfigurationDto configurationDto;
private SupportDto supportDto;

@BeforeEach
void setUp() {
this.configurationDto = new ConfigurationDto(List.of("ADMIN_1", "ADMIN_2"));
this.supportDto = new SupportDto("https://example.com");
}

@DisplayName("Should successfully get configuration")
@Test
void shouldSuccessfullyGetConfiguration() throws Exception {
when(mapper.toDto(authorizationConfiguration)).thenReturn(configurationDto);
when(configMapper.toDto(authorizationConfiguration)).thenReturn(configurationDto);

mvc
.perform(get(BASEURL).with(csrf()).accept(MediaType.APPLICATION_JSON))
.perform(get(BASEURL + "/authorization").with(csrf()).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(JsonDtoMatcher.matchesDto(configurationDto, "$"));
}

@DisplayName("Should successfully get support page url")
@Test
void shouldSuccessfullyGetSupportPageUrl() throws Exception {
when(helpUrlConfiguration.helpUrl()).thenReturn(supportUrl);
when(supportMapper.toDto(supportUrl)).thenReturn(supportDto);

mvc
.perform(get(BASEURL + "/help").with(csrf()).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(JsonDtoMatcher.matchesDto(supportDto, "$"));
Comment thread
fstreit1 marked this conversation as resolved.
Outdated
}

}
3 changes: 3 additions & 0 deletions backend/src/test/resources/application-test.properties
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ app.member-sync.cron=0 0 3 * * ?
app.member-sync.username=
app.member-sync.password=

# Support page url
pcts.app.help.url=https://google.com

# OAuth
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8544/realms/pitc
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8544/realms/pitc/protocol/openid-connect/certs
Expand Down
13 changes: 13 additions & 0 deletions frontend/cypress/e2e/topbar.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as users from '../fixtures/users.json';

describe('Topbar', () => {
beforeEach(() => {
cy.loginAsUser(users.gl);
cy.visit('/');
});

it('should have correct url on help', () => {
cy.get('#support-page-url')
Comment thread
fstreit1 marked this conversation as resolved.
Outdated
.should('have.attr', 'href', 'https://dummy-url.test');
Comment thread
fstreit1 marked this conversation as resolved.
Outdated
});
});
1 change: 1 addition & 0 deletions frontend/public/i18n/de.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"PCTS_LOGO_ALT": "Puzzle PCTS Logo",
"LOGOUT": "Abmelden",
"HELP": "Hilfe",
"DEGREE": {
"MODEL_NAME": "Ausbildung",
"NAME": "Bezeichnung"
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,21 @@
</pzsh-topbar>
<pzsh-menu class="position-fixed z-3" data-testid="user-options">
<div slot="actions">
<pzsh-menu-action>
<a class="text-reset text-decoration-none d-flex align-items-center" id="support-page-url" data-testid="support-page-url" href="{{ url() }}" target="_blank">
Comment thread
fstreit1 marked this conversation as resolved.
Outdated
<pzsh-icon class="pe-1" name="question-circle"></pzsh-icon>
{{ "HELP" | translate }}
</a>
</pzsh-menu-action>
<pzsh-menu-dropdown href="#">
<div slot="toggle">
<pzsh-icon name="user"></pzsh-icon>
{{ authService.name() }}
</div>
<div slot="items">
<pzsh-menu-dropdown-item href="#">
<button class="bg-transparent border-0 p-0 shadow-none text-reset d-inline-flex align-items-center" (click)="handleLogout()" data-testid="logout">
<button class="bg-transparent border-0 p-0 shadow-none text-reset d-inline-flex align-items-center"
Comment thread
fstreit1 marked this conversation as resolved.
(click)="handleLogout()" data-testid="logout">
<pzsh-icon name="sign-out-alt"></pzsh-icon>
{{ "LOGOUT" | translate }}
</button>
Expand Down
13 changes: 11 additions & 2 deletions frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Component, CUSTOM_ELEMENTS_SCHEMA, DOCUMENT, effect, inject } from '@angular/core';
import { Component, CUSTOM_ELEMENTS_SCHEMA, DOCUMENT, effect, inject, signal } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';
import '@puzzleitc/puzzle-shell';
import { LangChangeEvent, TranslatePipe, TranslateService } from '@ngx-translate/core';
import { NgOptimizedImage } from '@angular/common';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
import { AuthService } from './core/auth/auth.service';
import { ConfigurationService } from './features/configuration/configuration.service';

@Component({
selector: 'app-root',
Expand All @@ -23,6 +24,8 @@ export class AppComponent {

protected readonly authService = inject(AuthService);

protected readonly configService = inject(ConfigurationService);

private readonly translateService = inject(TranslateService);

private readonly document = inject(DOCUMENT);
Expand All @@ -31,10 +34,17 @@ export class AppComponent {
initialValue: this.translateService.getCurrentLang() || this.translateService.getFallbackLang() || this.translateService.getBrowserLang() || 'en'
});

readonly url = signal<string>('');
Comment thread
fstreit1 marked this conversation as resolved.
Outdated

constructor() {
effect(() => {
this.setHtmlLangAttribute(this.currentLang());
});

this.configService.getHelpUrl()
.subscribe((response) => {
this.url.set(response.url);
});
}

visitRoot(): void {
Expand All @@ -51,4 +61,3 @@ export class AppComponent {
}
}
}

3 changes: 3 additions & 0 deletions frontend/src/app/features/configuration/HelpUrl.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface HelpUrlModel {
url: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { ConfigurationService } from './configuration.service';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { ConfigurationModel } from './configuration.model';
import { configuration } from '../../shared/test/test-data';
import { configuration, helpUrl } from '../../shared/test/test-data';
import { HelpUrlModel } from './HelpUrl.model';

describe('configurationService', () => {
let httpMock: HttpTestingController;
let service: ConfigurationService;
const API_URL = '/api/v1/configuration';
const API_URL = '/api/v1/configuration/authorization';

beforeEach(() => {
TestBed.configureTestingModule({
Expand Down Expand Up @@ -47,4 +48,21 @@ describe('configurationService', () => {
req.flush(mockConfig);
});
});

describe('Supportpageurl endpoint', () => {
Comment thread
fstreit1 marked this conversation as resolved.
it('should fetch url', () => {
const mockUrl: HelpUrlModel = helpUrl;

service.getConfiguration()
.subscribe((response) => {
expect(response)
.toEqual(mockUrl);
});

const req = httpMock.expectOne(API_URL);
expect(req.request.method)
.toBe('GET');
req.flush(mockUrl);
});
});
});
Loading
Loading