From 6b3ffc61733f3a2954db603542be0194c49a03b6 Mon Sep 17 00:00:00 2001 From: "Donald F. Coffin" Date: Sat, 14 Nov 2026 13:22:05 -0500 Subject: [PATCH] feat(ui): finish the GBA theme pass on the Data Custodian; theme tests Follow-up to #244. - Data Custodian admin dashboard: GBA-green action buttons and icons (the rainbow btn-success/info/danger/warning are gone) and white "stat" tiles in place of the coloured cards. Login icons and the error pages' "Try Again" button use the palette; the error icon size moves from inline styles to a theme class. Semantic colours stay: status badges, Delete/Revoke, warning and danger error icons, neutral Cancel/Go Back. - Footer moved out of the page container in 18 templates (and the leftover
separators removed), so it is a full-width child of pinned to the bottom of short pages. The two legacy customer pages (#245) are untouched; the theme keeps a full-bleed rule for them until #245. - application.css reduced to the rules still used that the theme does not cover (table header, focus outline, print). The old Material palette, radii, hover lifts, gradients and light footer were fighting the theme, so the theme drops most of its !important overrides and the --gb-* bridge. - Theme: white inputs and tables (Bootstrap fills them with the grey body colour), stat-tile and status-icon classes, page content grows so the footer sits at the bottom. - Retail customers list: the Delete button is btn-sm like View/Edit (btn-group-sm does not reach a button nested in a form). - Tests: GbaThemeResourcesTest (DC) and GbaLayoutPagesTest (TP, the first TP test with a full Spring context) check that the theme, logo and favicons are served anonymously (the logo byte-for-byte, so resource filtering cannot corrupt it), that pages render the GBA layout with SRI-pinned assets, and that the TP public pages are reachable without login. Co-Authored-By: Claude Opus 5.5 --- .../META-INF/resources/css/gba-theme.css | 69 +++-- .../main/resources/static/css/application.css | 264 +----------------- .../src/main/resources/templates/about.html | 4 +- .../resources/templates/authorize-screen.html | 4 +- .../resources/templates/custodian/home.html | 86 +++--- .../templates/custodian/notifications.html | 4 +- .../templates/custodian/oauth/tokens.html | 4 +- .../templates/custodian/password.html | 4 +- .../custodian/retailcustomers/form.html | 4 +- .../custodian/retailcustomers/index.html | 7 +- .../custodian/retailcustomers/show.html | 4 +- .../templates/custodian/settings.html | 4 +- .../resources/templates/custodian/upload.html | 4 +- .../templates/customer/authorizations.html | 4 +- .../templates/customer/password.html | 4 +- .../src/main/resources/templates/error.html | 10 +- .../main/resources/templates/error/400.html | 6 +- .../main/resources/templates/error/403.html | 6 +- .../src/main/resources/templates/home.html | 6 +- .../src/main/resources/templates/login.html | 10 +- .../web/GbaThemeResourcesTest.java | 83 ++++++ .../thirdparty/web/GbaLayoutPagesTest.java | 70 +++++ 22 files changed, 296 insertions(+), 365 deletions(-) create mode 100644 openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/GbaThemeResourcesTest.java create mode 100644 openespi-thirdparty/src/test/java/org/greenbuttonalliance/espi/thirdparty/web/GbaLayoutPagesTest.java diff --git a/openespi-common/src/main/resources/META-INF/resources/css/gba-theme.css b/openespi-common/src/main/resources/META-INF/resources/css/gba-theme.css index 7ce8927d..ab86109e 100644 --- a/openespi-common/src/main/resources/META-INF/resources/css/gba-theme.css +++ b/openespi-common/src/main/resources/META-INF/resources/css/gba-theme.css @@ -58,12 +58,6 @@ --bs-border-radius-sm: 0; --bs-border-radius-lg: 0; --bs-focus-ring-color: rgba(249, 151, 66, 0.45); - - /* Data Custodian application.css tokens, so its existing rules adopt the GBA palette */ - --gb-primary: var(--gba-green-900); - --gb-secondary: var(--gba-green-700); - --gb-accent: var(--gba-green-700); - --gb-warning: var(--gba-orange); } body[data-gba-app="thirdparty"] { @@ -79,7 +73,9 @@ body { min-height: 100vh; } -main { +/* The page content grows so the footer sits at the bottom of short pages. */ +main, +body > .container { flex: 1 0 auto; } @@ -172,11 +168,6 @@ h1, h2, h3, h4, h5, h6, --bs-btn-border-width: 2px; --bs-btn-font-weight: 500; --bs-btn-font-family: var(--gba-font); - border-radius: 0; /* the Data Custodian application.css sets a literal 0.5rem */ -} - -.btn:hover { - transform: none; } .btn-primary { @@ -236,12 +227,13 @@ h1, h2, h3, h4, h5, h6, .gba-hero, .hero-unit { + /* !important beats Bootstrap's !important utilities (bg-*, text-*, rounded) that hero markup + still carries; hero backgrounds are always dark, so the text must stay white. */ background: var(--gba-hero-bg) !important; - /* !important: hero backgrounds are always dark, so override text-dark and similar utilities */ color: #fff !important; border-left: 6px solid var(--gba-orange); border-radius: 0 !important; - box-shadow: none !important; + margin-bottom: 2rem; padding: 3rem; } @@ -266,20 +258,13 @@ h1, h2, h3, h4, h5, h6, .card { --bs-card-bg: #FFFFFF; /* white cards on the grey page, as on greenbuttonalliance.org */ - border: 1px solid #DDDDDD; - border-radius: 0 !important; - box-shadow: none; + --bs-card-border-color: #DDDDDD; } .card:hover { - transform: none; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } -.card-header { - border-radius: 0 !important; -} - .gba-card-header { background-color: var(--gba-green-900); color: #fff; @@ -300,8 +285,42 @@ h1, h2, h3, h4, h5, h6, color: var(--gba-green-700); } +/* Large status icon on error pages (keeps its semantic text-warning / text-danger colour) */ +.gba-status-icon { + font-size: 5rem; + line-height: 1; +} + +/* Dashboard statistic tile: white card with a green left rule and a large figure */ +.gba-stat { + border-left: 6px solid var(--gba-green-900); +} + +.gba-stat .gba-stat-value { + color: var(--gba-green-900); + font-size: 2.25rem; + font-weight: 700; + line-height: 1.1; +} + +.gba-stat .gba-stat-label { + color: var(--gba-text); + font-weight: 500; +} + /* ---- Forms ----------------------------------------------------------------------------- */ +/* Bootstrap fills inputs and tables with --bs-body-bg, which the theme sets to the grey page + colour; keep them white, as on greenbuttonalliance.org. */ +.form-control, +.form-select { + background-color: #FFFFFF; +} + +.table { + --bs-table-bg: #FFFFFF; +} + .form-control:focus, .form-select:focus { border-color: var(--gba-orange); @@ -318,8 +337,10 @@ h1, h2, h3, h4, h5, h6, padding: 2rem 0 1.5rem; } -/* Data Custodian templates place the footer inside the page .container; let it span the full - viewport anyway (body clips the scrollbar-width overshoot of 100vw). */ +/* Pages should place the footer after the page container, as a child of . The two legacy + Data Custodian customer pages (customer/home, customer/usagepoints/index; #245) still place it + inside the .container, so let it span the full viewport there too (body clips the + scrollbar-width overshoot of 100vw). Remove once #245 rebuilds those pages. */ .container > .gba-footer { margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw); diff --git a/openespi-datacustodian/src/main/resources/static/css/application.css b/openespi-datacustodian/src/main/resources/static/css/application.css index fe463f47..a742f1f6 100644 --- a/openespi-datacustodian/src/main/resources/static/css/application.css +++ b/openespi-datacustodian/src/main/resources/static/css/application.css @@ -1,278 +1,44 @@ /* * Green Button Data Custodian Application Styles * Copyright (c) 2018-2026 Green Button Alliance, Inc. + * + * Colours, typography, header, footer, buttons, cards and heroes come from the shared Green Button + * Alliance theme (/css/gba-theme.css, shipped in OpenESPI-Common), which fragments/layout loads + * after this file. Keep only Data Custodian-specific rules here and use the theme's --gba-* tokens; + * do not redefine the palette, corner radii or component chrome. */ -/* Custom Green Button Color Palette */ -:root { - --gb-primary: #2E7D32; /* Green Button Green */ - --gb-secondary: #388E3C; /* Lighter Green */ - --gb-accent: #4CAF50; /* Success Green */ - --gb-warning: #FF9800; /* Orange */ - --gb-danger: #F44336; /* Red */ - --gb-info: #2196F3; /* Blue */ - --gb-light: #F5F5F5; /* Light Gray */ - --gb-dark: #212121; /* Dark Gray */ -} - -/* Override Bootstrap Primary Colors */ -.bg-primary { - background-color: var(--gb-primary) !important; -} - -.btn-primary { - background-color: var(--gb-primary); - border-color: var(--gb-primary); -} - -.btn-primary:hover { - background-color: var(--gb-secondary); - border-color: var(--gb-secondary); -} - -.navbar-brand { - font-weight: 600; - font-size: 1.5rem; -} - -/* Hero Unit Styling */ -.hero-unit { - margin-bottom: 2rem; - background: linear-gradient(135deg, var(--gb-primary) 0%, var(--gb-secondary) 100%); - border-radius: 0.5rem; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); -} - -.hero-unit h1, .hero-unit h2 { - margin-bottom: 1rem; - font-weight: 600; -} - -.hero-unit .lead { - font-size: 1.25rem; - font-weight: 300; -} - -/* Card Enhancements */ -.card { - border: none; - border-radius: 0.75rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - transition: all 0.3s ease; -} - -.card:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); -} - -.card-header { - border-radius: 0.75rem 0.75rem 0 0 !important; - font-weight: 600; -} - -/* Navigation Enhancements */ -.navbar { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.nav-link { - font-weight: 500; - transition: all 0.3s ease; -} - -.nav-link:hover { - transform: translateY(-1px); -} - -/* Form Styling */ -.form-control { - border-radius: 0.5rem; - border: 2px solid #e0e0e0; - transition: all 0.3s ease; -} - -.form-control:focus { - border-color: var(--gb-primary); - box-shadow: 0 0 0 0.2rem rgba(46, 125, 50, 0.25); -} - -.btn { - border-radius: 0.5rem; - font-weight: 500; - transition: all 0.3s ease; -} - -.btn:hover { - transform: translateY(-1px); -} - -/* Dashboard Statistics Cards */ -.stats-card { - background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); - border-left: 4px solid var(--gb-primary); -} - -/* Alert Styling */ -.alert { - border-radius: 0.5rem; - border: none; - font-weight: 500; -} - -.alert-success { - background-color: rgba(76, 175, 80, 0.1); - color: var(--gb-primary); -} - -.alert-danger { - background-color: rgba(244, 67, 54, 0.1); - color: var(--gb-danger); -} - -.alert-warning { - background-color: rgba(255, 152, 0, 0.1); - color: var(--gb-warning); -} - -.alert-info { - background-color: rgba(33, 150, 243, 0.1); - color: var(--gb-info); -} - -/* Footer Styling */ -footer { - background-color: var(--gb-light); - border-top: 1px solid #e0e0e0; -} - -footer a { - color: var(--gb-primary); - text-decoration: none; - font-weight: 500; -} - -footer a:hover { - color: var(--gb-secondary); - text-decoration: underline; -} - -/* Error Page Styling */ -.error-page { - min-height: 60vh; - display: flex; - align-items: center; - justify-content: center; -} - -/* Loading Spinner */ -.spinner-border-green { - color: var(--gb-primary); -} - -/* Data Table Enhancements */ -.table { - border-radius: 0.5rem; - overflow: hidden; -} - +/* Data tables: GBA dark-green header row */ .table thead th { - background-color: var(--gb-primary); - color: white; + background-color: var(--gba-green-900); + color: #FFFFFF; border: none; - font-weight: 600; -} - -.table tbody tr:hover { - background-color: rgba(46, 125, 50, 0.05); -} - -/* Badge Styling */ -.badge { - border-radius: 0.5rem; font-weight: 500; } -.badge-success { - background-color: var(--gb-accent); -} - -.badge-primary { - background-color: var(--gb-primary); -} - -/* Progress Bar */ -.progress { - border-radius: 0.5rem; - height: 0.75rem; -} - -.progress-bar { - background-color: var(--gb-primary); -} - -/* Responsive Adjustments */ -@media (max-width: 768px) { - .hero-unit { - padding: 2rem 1.5rem; - text-align: center; - } - - .hero-unit h1 { - font-size: 2rem; - } - - .hero-unit h2 { - font-size: 1.5rem; - } - - .navbar-brand { - font-size: 1.25rem; - } -} - -/* Animation Classes */ -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } -} - -.fade-in { - animation: fadeIn 0.6s ease-out; -} - -/* Utility Classes */ -.text-green { - color: var(--gb-primary) !important; -} - -.bg-green { - background-color: var(--gb-primary) !important; -} - -.border-green { - border-color: var(--gb-primary) !important; +.table tbody tr:hover { + background-color: var(--gba-green-050); } -/* Focus Visible for Accessibility */ +/* Focus visible for accessibility (orange, as the theme's focus ring) */ .btn:focus-visible, .nav-link:focus-visible, .form-control:focus-visible { - outline: 2px solid var(--gb-primary); + outline: 2px solid var(--gba-orange-700); outline-offset: 2px; } -/* Print Styles */ +/* Print: content only */ @media print { .navbar, .btn, footer { display: none !important; } - + .container { max-width: none !important; margin: 0; padding: 0; } -} \ No newline at end of file +} diff --git a/openespi-datacustodian/src/main/resources/templates/about.html b/openespi-datacustodian/src/main/resources/templates/about.html index 7f95c011..d2449459 100644 --- a/openespi-datacustodian/src/main/resources/templates/about.html +++ b/openespi-datacustodian/src/main/resources/templates/about.html @@ -27,8 +27,8 @@

About

-
-
+ +
diff --git a/openespi-datacustodian/src/main/resources/templates/authorize-screen.html b/openespi-datacustodian/src/main/resources/templates/authorize-screen.html index 0aac41a2..715c9247 100644 --- a/openespi-datacustodian/src/main/resources/templates/authorize-screen.html +++ b/openespi-datacustodian/src/main/resources/templates/authorize-screen.html @@ -103,10 +103,10 @@
Personal information
- -
+
+ diff --git a/openespi-datacustodian/src/main/resources/templates/custodian/home.html b/openespi-datacustodian/src/main/resources/templates/custodian/home.html index 9a967da0..7d11e137 100644 --- a/openespi-datacustodian/src/main/resources/templates/custodian/home.html +++ b/openespi-datacustodian/src/main/resources/templates/custodian/home.html @@ -9,7 +9,7 @@
-
+

Data Custodian Dashboard

Manage retail customers, monitor OAuth authorizations, and administer Green Button data access. @@ -21,7 +21,7 @@

Data Custodian Dashboard

- +
Retail Customers

Manage customer accounts and their usage points.

Manage Customers @@ -32,10 +32,10 @@
Retail Customers
- +
OAuth Tokens

Monitor and manage OAuth access tokens.

- View Tokens + View Tokens
@@ -43,10 +43,10 @@
OAuth Tokens
- +
Data Upload

Upload customer usage data in bulk.

- Upload Data + Upload Data
@@ -54,10 +54,10 @@
Data Upload
- +
Notify Third Party

Send an ESPI BatchList of resource URLs to a third party.

- Notify + Notify
@@ -65,10 +65,10 @@
Notify Third Party
- +
System Settings

Configure system-wide settings and preferences.

- Settings + Settings
@@ -77,70 +77,60 @@
System Settings
-

System Overview

+

System Overview

- +
-
-
-
-
-

0

-

Total Customers

-
- +
+
+
+
0
+
Total Customers
+
-
-
-
-
-

0

-

Active Tokens

-
- +
+
+
+
0
+
Active Tokens
+
-
-
-
-
-

0

-

Usage Points

-
- +
+
+
+
0
+
Usage Points
+
-
-
-
-
-

0

-

Today's API Requests

-
- +
+
+
+
0
+
Today's API Requests
+
- -
- -
+
+ \ No newline at end of file diff --git a/openespi-datacustodian/src/main/resources/templates/custodian/notifications.html b/openespi-datacustodian/src/main/resources/templates/custodian/notifications.html index a4eb9648..34318779 100644 --- a/openespi-datacustodian/src/main/resources/templates/custodian/notifications.html +++ b/openespi-datacustodian/src/main/resources/templates/custodian/notifications.html @@ -109,8 +109,8 @@
BatchList resources — select which to send
-
-
+ +
diff --git a/openespi-datacustodian/src/main/resources/templates/custodian/oauth/tokens.html b/openespi-datacustodian/src/main/resources/templates/custodian/oauth/tokens.html index 9f81eaea..ab3381ca 100644 --- a/openespi-datacustodian/src/main/resources/templates/custodian/oauth/tokens.html +++ b/openespi-datacustodian/src/main/resources/templates/custodian/oauth/tokens.html @@ -47,8 +47,8 @@

OAuth Token Management

-
-
+ +
diff --git a/openespi-datacustodian/src/main/resources/templates/custodian/password.html b/openespi-datacustodian/src/main/resources/templates/custodian/password.html index b73e1df2..f2dd7e76 100644 --- a/openespi-datacustodian/src/main/resources/templates/custodian/password.html +++ b/openespi-datacustodian/src/main/resources/templates/custodian/password.html @@ -36,8 +36,8 @@

Change Password

-
-
+ +
diff --git a/openespi-datacustodian/src/main/resources/templates/custodian/retailcustomers/form.html b/openespi-datacustodian/src/main/resources/templates/custodian/retailcustomers/form.html index 8171c654..3e0122d2 100644 --- a/openespi-datacustodian/src/main/resources/templates/custodian/retailcustomers/form.html +++ b/openespi-datacustodian/src/main/resources/templates/custodian/retailcustomers/form.html @@ -64,8 +64,8 @@

-

+ +
diff --git a/openespi-datacustodian/src/main/resources/templates/custodian/retailcustomers/index.html b/openespi-datacustodian/src/main/resources/templates/custodian/retailcustomers/index.html index 9dd6ae3c..86957392 100644 --- a/openespi-datacustodian/src/main/resources/templates/custodian/retailcustomers/index.html +++ b/openespi-datacustodian/src/main/resources/templates/custodian/retailcustomers/index.html @@ -53,7 +53,8 @@

Retail Customers

- + +
@@ -66,8 +67,8 @@

Retail Customers

-
-
+ +
diff --git a/openespi-datacustodian/src/main/resources/templates/custodian/retailcustomers/show.html b/openespi-datacustodian/src/main/resources/templates/custodian/retailcustomers/show.html index 51503f57..5679d44f 100644 --- a/openespi-datacustodian/src/main/resources/templates/custodian/retailcustomers/show.html +++ b/openespi-datacustodian/src/main/resources/templates/custodian/retailcustomers/show.html @@ -64,8 +64,8 @@

Customer Name

write was not part of the resource-server migration).
-
-
+ +
diff --git a/openespi-datacustodian/src/main/resources/templates/custodian/settings.html b/openespi-datacustodian/src/main/resources/templates/custodian/settings.html index 5ca539e7..903ba616 100644 --- a/openespi-datacustodian/src/main/resources/templates/custodian/settings.html +++ b/openespi-datacustodian/src/main/resources/templates/custodian/settings.html @@ -39,8 +39,8 @@

System Settings

-
-
+ +
diff --git a/openespi-datacustodian/src/main/resources/templates/custodian/upload.html b/openespi-datacustodian/src/main/resources/templates/custodian/upload.html index 2909e622..385b9d8b 100644 --- a/openespi-datacustodian/src/main/resources/templates/custodian/upload.html +++ b/openespi-datacustodian/src/main/resources/templates/custodian/upload.html @@ -30,8 +30,8 @@

Data Upload

-
-
+ +
diff --git a/openespi-datacustodian/src/main/resources/templates/customer/authorizations.html b/openespi-datacustodian/src/main/resources/templates/customer/authorizations.html index d21660da..c3b8f4a0 100644 --- a/openespi-datacustodian/src/main/resources/templates/customer/authorizations.html +++ b/openespi-datacustodian/src/main/resources/templates/customer/authorizations.html @@ -57,8 +57,8 @@

My Authorizations

-
-
+ +
diff --git a/openespi-datacustodian/src/main/resources/templates/customer/password.html b/openespi-datacustodian/src/main/resources/templates/customer/password.html index e7c4878e..3e0e5719 100644 --- a/openespi-datacustodian/src/main/resources/templates/customer/password.html +++ b/openespi-datacustodian/src/main/resources/templates/customer/password.html @@ -36,8 +36,8 @@

Change Password

-
-
+ +
diff --git a/openespi-datacustodian/src/main/resources/templates/error.html b/openespi-datacustodian/src/main/resources/templates/error.html index c036dacd..c2340dd5 100644 --- a/openespi-datacustodian/src/main/resources/templates/error.html +++ b/openespi-datacustodian/src/main/resources/templates/error.html @@ -11,7 +11,7 @@
- +

Error

Something went wrong

@@ -35,13 +35,13 @@
Error Details:
Go Home - - + +
- -
+ +
\ No newline at end of file diff --git a/openespi-datacustodian/src/main/resources/templates/error/400.html b/openespi-datacustodian/src/main/resources/templates/error/400.html index 9da95f40..0bbbec93 100644 --- a/openespi-datacustodian/src/main/resources/templates/error/400.html +++ b/openespi-datacustodian/src/main/resources/templates/error/400.html @@ -11,7 +11,7 @@
- +

400

Bad Request

@@ -24,8 +24,8 @@

Bad Request

- -
+ +
\ No newline at end of file diff --git a/openespi-datacustodian/src/main/resources/templates/error/403.html b/openespi-datacustodian/src/main/resources/templates/error/403.html index 42ecf2e9..501c6aa1 100644 --- a/openespi-datacustodian/src/main/resources/templates/error/403.html +++ b/openespi-datacustodian/src/main/resources/templates/error/403.html @@ -11,7 +11,7 @@
- +

403

Access Forbidden

@@ -25,8 +25,8 @@

Access Forbidden

- -
+ +
\ No newline at end of file diff --git a/openespi-datacustodian/src/main/resources/templates/home.html b/openespi-datacustodian/src/main/resources/templates/home.html index ee25b056..a143ca01 100644 --- a/openespi-datacustodian/src/main/resources/templates/home.html +++ b/openespi-datacustodian/src/main/resources/templates/home.html @@ -9,7 +9,7 @@
- +
+ diff --git a/openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/GbaThemeResourcesTest.java b/openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/GbaThemeResourcesTest.java new file mode 100644 index 00000000..2737f3f7 --- /dev/null +++ b/openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/GbaThemeResourcesTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2026 Green Button Alliance, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenbuttonalliance.espi.datacustodian.web; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * The shared Green Button Alliance theme, logo and favicon ship in the OpenESPI-Common jar + * (META-INF/resources) and must be served anonymously, and the portal layout must reference them. + * Guards both the DC resource handlers (@EnableWebMvc disables Boot's default static mappings) and + * the root-pom resource packaging that once dropped every binary file from the jars. + */ +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class GbaThemeResourcesTest { + + @Autowired + private MockMvc mockMvc; + + @ParameterizedTest + @CsvSource({ + "/css/gba-theme.css, text/css", + "/images/gba-logo.png, image/png", + "/images/gba-favicon.png, image/png", + "/images/favicon.png, image/png"}) + void shared_assets_are_served_anonymously(String path, String contentType) throws Exception { + byte[] body = mockMvc.perform(get(path)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(contentType)) + .andReturn().getResponse().getContentAsByteArray(); + + assertThat(body).isNotEmpty(); + } + + @Test + void logo_is_packaged_unfiltered() throws Exception { + byte[] logo = mockMvc.perform(get("/images/gba-logo.png")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsByteArray(); + + // PNG signature: resource filtering would have corrupted these bytes + assertThat(logo).startsWith(0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A); + } + + @Test + void home_page_uses_the_gba_layout() throws Exception { + String html = mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html) + .contains("/css/gba-theme.css", "/images/gba-logo.png", "/images/gba-favicon.png") + .contains("gba-navbar", "gba-footer") + .contains("integrity=\"sha384-"); + } +} diff --git a/openespi-thirdparty/src/test/java/org/greenbuttonalliance/espi/thirdparty/web/GbaLayoutPagesTest.java b/openespi-thirdparty/src/test/java/org/greenbuttonalliance/espi/thirdparty/web/GbaLayoutPagesTest.java new file mode 100644 index 00000000..372847b5 --- /dev/null +++ b/openespi-thirdparty/src/test/java/org/greenbuttonalliance/espi/thirdparty/web/GbaLayoutPagesTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026 Green Button Alliance, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenbuttonalliance.espi.thirdparty.web; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Third Party pages render through fragments/layout (shared GBA theme, logo, context-relative + * links) and the public pages linked from the navbar are reachable without logging in. + */ +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class GbaLayoutPagesTest { + + @Autowired + private MockMvc mockMvc; + + @ParameterizedTest + @ValueSource(strings = {"/", "/login", "/TermsOfService", "/UsagePolicy"}) + void public_pages_render_anonymously_with_the_gba_layout(String path) throws Exception { + String html = mockMvc.perform(get(path)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertThat(html) + .contains("/css/gba-theme.css", "/images/gba-logo.png", "/images/gba-favicon.png") + .contains("gba-navbar", "gba-footer", "data-gba-app=\"thirdparty\"") + .contains("integrity=\"sha384-") + // no root-relative internal links (#235): the layout uses th:href="@{...}" + .doesNotContain("href=\"/about\""); + } + + @ParameterizedTest + @CsvSource({ + "/css/gba-theme.css, text/css", + "/images/gba-logo.png, image/png", + "/images/gba-favicon.png, image/png"}) + void shared_assets_are_served_anonymously(String path, String contentType) throws Exception { + mockMvc.perform(get(path)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(contentType)); + } +}