Skip to content

Commit e5a7777

Browse files
authored
Merge pull request #235 from HydrologicEngineeringCenter/156-blob-endpoint-fails-to-store-excelfails-to-determine-encoding
add test for excel files
2 parents bfcca3a + 9535a74 commit e5a7777

File tree

4 files changed

+75
-51
lines changed

4 files changed

+75
-51
lines changed

.github/workflows/CDA-testing.yml

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: CI
22

33
on:
44
push:
5-
branches: [main, githubAction-testing]
5+
branches: [main]
66
pull_request:
77
branches: [main]
88
workflow_dispatch:
@@ -14,40 +14,42 @@ jobs:
1414
steps:
1515
- uses: actions/checkout@v5
1616

17-
- name: set up backend
18-
run: docker compose up --build -d
17+
- name: Set up backend
18+
run: |
19+
docker compose pull
20+
docker compose up -d
1921
2022
- name: Set Up Python
2123
uses: actions/setup-python@v6
2224
with:
2325
python-version: '3.9.X'
2426

25-
# Unlike the code-check workflow, this job requires the dev dependencies to be
26-
# installed to make sure we have the necessary, tools, stub files, etc.
27-
- name: Install Poetry
27+
# Use actions-poetry to handle installation
28+
- name: Install Poetry and Dependencies
2829
uses: abatilo/actions-poetry@v4
2930

30-
- name: Add Poetry to PATH (for act)
31-
if: env.ACT
32-
run: echo "/root/.local/bin" >> $GITHUB_PATH
31+
# Set Poetry to use an in-project virtual environment
32+
- name: Configure Poetry for in-project venv
33+
run: poetry config virtualenvs.in-project true
3334

34-
- name: Cache Virtual Environment
35+
# Poetry will handle installation and caching
36+
- name: Cache Python dependencies
3537
uses: actions/cache@v4
38+
id: cache-poetry-venv
3639
with:
37-
path: ./.venv
38-
key: venv-${{ hashFiles('poetry.lock') }}
40+
path: .venv
41+
key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }}
3942

40-
- name: Install Dependencies
41-
run: poetry install
43+
# Install dependencies only if cache is missed
44+
- name: Install dependencies
45+
if: steps.cache-poetry-venv.outputs.cache-hit != 'true'
46+
run: poetry install --no-root
4247

4348
# Run pytest and generate coverage report data.
44-
- name: Run Tests
45-
run: poetry run pytest tests/cda/ --doctest-modules --cov --cov-report=xml:out/coverage.xml
46-
47-
# Run mypy with strict mode enabled. Only the main source code is type checked (test
48-
# and example code is excluded).
49-
- name: Check Types
50-
run: poetry run mypy --strict cwms/
49+
- name: Run Tests and Check Types
50+
run: |
51+
poetry run pytest tests/cda/ --doctest-modules --cov --cov-report=xml:out/coverage.xml
52+
poetry run mypy --strict cwms/
5153
5254
- name: Generate Coverage Report
5355
uses: irongut/CodeCoverageSummary@v1.3.0

docker-compose.yml

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ services:
1010
- CWMS_PASSWORD=simplecwmspasswD1
1111
- OFFICE_ID=HQ
1212
- OFFICE_EROC=s0
13-
ports:
14-
- "1526:1521"
13+
ports: ["1526:1521"]
1514
healthcheck:
16-
test: ["CMD","tnsping", "FREEPDB1"]
15+
test: ["CMD", "tnsping", "FREEPDB1"]
1716
interval: 30s
1817
timeout: 50s
1918
retries: 50
@@ -32,9 +31,9 @@ services:
3231
- INSTALLONCE=1
3332
- QUIET=1
3433
command: >
35-
sh -xc "sqlplus CWMS_20/$$CWMS_PASSWORD@$$DB_HOST_PORT$$DB_NAME @/setup_sql/users $$OFFICE_EROC"
36-
volumes:
37-
- ./compose_files/sql:/setup_sql:ro
34+
sh -xc "sqlplus CWMS_20/$$CWMS_PASSWORD@$$DB_HOST_PORT$$DB_NAME @/setup_sql/users
35+
$$OFFICE_EROC"
36+
volumes: [./compose_files/sql:/setup_sql:ro]
3837
depends_on:
3938
db:
4039
condition: service_healthy
@@ -43,7 +42,6 @@ services:
4342
traefik:
4443
condition: service_healthy
4544

46-
4745
data-api:
4846
depends_on:
4947
auth:
@@ -75,10 +73,9 @@ services:
7573
- cwms.dataapi.access.openid.altAuthUrl=http://localhost:${APP_PORT:-8082}
7674
- cwms.dataapi.access.openid.useAltWellKnown=true
7775
- cwms.dataapi.access.openid.issuer=http://localhost:${APP_PORT:-8082}/auth/realms/cwms
78-
expose:
79-
- 7000
76+
expose: [7000]
8077
healthcheck:
81-
test: ["CMD","/usr/bin/curl", "-I","localhost:7000/cwms-data/offices/HEC"]
78+
test: ["CMD", "/usr/bin/curl", "-I", "localhost:7000/cwms-data/offices/HEC"]
8279
interval: 5s
8380
timeout: 1s
8481
retries: 100
@@ -90,9 +87,12 @@ services:
9087

9188
auth:
9289
image: quay.io/keycloak/keycloak:19.0.1
93-
command: ["start-dev", "--features-disabled=admin2","--import-realm"]
90+
command: ["start-dev", "--import-realm"]
9491
healthcheck:
95-
test: "/usr/bin/curl -If localhost:${APP_PORT:-8082}/auth/health/ready || exit 1"
92+
test:
93+
- "CMD-SHELL"
94+
- "/usr/bin/curl -If localhost:${APP_PORT:-8082}/auth/health/ready || exit\
95+
\ 1"
9696
interval: 5s
9797
timeout: 1s
9898
retries: 100
@@ -108,6 +108,8 @@ services:
108108
- KC_PROXY=none
109109
- KC_HTTP_ENABLED=true
110110
- KC_HTTP_RELATIVE_PATH=/auth
111+
- KC_HOSTNAME=localhost
112+
- KC_DB=dev-file
111113
volumes:
112114
- ./compose_files/keycloak/realm.json:/opt/keycloak/data/import/realm.json:ro
113115
labels:
@@ -119,17 +121,12 @@ services:
119121
traefik:
120122
condition: service_healthy
121123

122-
123-
124124
# Proxy for HTTPS for OpenID
125125
traefik:
126126
image: traefik:v3.3.3
127-
ports:
128-
- "${APP_PORT:-8082}:80"
129-
expose:
130-
- "8080"
131-
volumes:
132-
- "/var/run/docker.sock:/var/run/docker.sock:ro"
127+
ports: ["${APP_PORT:-8082}:80"]
128+
expose: ["8080"]
129+
volumes: ["/var/run/docker.sock:/var/run/docker.sock:ro"]
133130
healthcheck:
134131
test: traefik healthcheck --ping
135132
command:
@@ -142,4 +139,4 @@ services:
142139
labels:
143140
- "traefik.enable=true"
144141
- "traefik.http.routers.traefik.rule=PathPrefix(`/traefik`)"
145-
- "traefik.http.routers.traefik.service=api@internal"
142+
- "traefik.http.routers.traefik.service=api@internal"

tests/cda/blobs/blob_CDA_test.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
# tests/test_blob.py
22
from __future__ import annotations
33

4+
import base64
5+
import mimetypes
46
from datetime import datetime, timezone
7+
from pathlib import Path
58
from typing import Optional
69

710
import pandas as pd
811
import pytest
912

10-
import cwms
13+
import cwms.catalog.blobs as blobs
1114

1215
TEST_OFFICE = "MVP"
1316
TEST_BLOB_ID = "PYTEST_BLOB_ALPHA"
@@ -25,12 +28,12 @@
2528
def ensure_clean_slate():
2629
"""Delete the test blob (if it exists) before/after running this module."""
2730
try:
28-
cwms.delete_blob(office_id=TEST_OFFICE, blob_id=TEST_BLOB_ID)
31+
blobs.delete_blob(office_id=TEST_OFFICE, blob_id=TEST_BLOB_ID)
2932
except Exception:
3033
pass
3134
yield
3235
try:
33-
cwms.delete_blob(office_id=TEST_OFFICE, blob_id=TEST_BLOB_ID)
36+
blobs.delete_blob(office_id=TEST_OFFICE, blob_id=TEST_BLOB_ID)
3437
except Exception:
3538
pass
3639

@@ -44,7 +47,7 @@ def _find_blob_row(office: str, blob_id: str) -> Optional[pd.Series]:
4447
"""
4548
Helper: return the row for blob_id from cwms.get_blobs(...).df if present.
4649
"""
47-
res = cwms.get_blobs(office_id=office, blob_id_like=blob_id)
50+
res = blobs.get_blobs(office_id=office, blob_id_like=blob_id)
4851
df = res if isinstance(res, pd.DataFrame) else getattr(res, "df", None)
4952
if df is None or df.empty:
5053
return None
@@ -55,6 +58,28 @@ def _find_blob_row(office: str, blob_id: str) -> Optional[pd.Series]:
5558
return match.iloc[0] if not match.empty else None
5659

5760

61+
def test_store_blob_excel():
62+
excel_file_path = Path(__file__).parent.parent / "resources" / "blob_test.xlsx"
63+
with open(excel_file_path, "rb") as f:
64+
file_data = f.read()
65+
mime_type, _ = mimetypes.guess_type(excel_file_path)
66+
excel_blob_id = "TEST_BLOB_EXCEL"
67+
payload = {
68+
"office-id": TEST_OFFICE,
69+
"id": excel_blob_id,
70+
"description": "testing excel file",
71+
"media-type-id": mime_type,
72+
"value": base64.b64encode(file_data).decode("utf-8"),
73+
}
74+
blobs.store_blobs(data=payload)
75+
try:
76+
row = _find_blob_row(TEST_OFFICE, excel_blob_id)
77+
assert row is not None, "Stored blob not found in listing"
78+
finally:
79+
# Cleanup excel
80+
blobs.delete_blob(blob_id=excel_blob_id, office_id=TEST_OFFICE)
81+
82+
5883
def test_store_blob():
5984
# Build request JSON for store_blobs
6085
payload = {
@@ -64,7 +89,7 @@ def test_store_blob():
6489
"media-type-id": TEST_MEDIA_TYPE,
6590
"value": TEST_TEXT,
6691
}
67-
cwms.store_blobs(payload, fail_if_exists=True)
92+
blobs.store_blobs(payload, fail_if_exists=True)
6893

6994
# Verify via listing metadata
7095
row = _find_blob_row(TEST_OFFICE, TEST_BLOB_ID)
@@ -76,14 +101,14 @@ def test_store_blob():
76101
assert TEST_DESC in str(row["description"])
77102

78103
# Verify content by downloading
79-
content = cwms.get_blob(office_id=TEST_OFFICE, blob_id=TEST_BLOB_ID)
104+
content = blobs.get_blob(office_id=TEST_OFFICE, blob_id=TEST_BLOB_ID)
80105
assert isinstance(content, str) and content, "Empty blob content"
81106
assert TEST_TEXT in content
82107

83108

84109
def test_get_blob():
85110
# Do a simple read of the blob created in test_store_blob
86-
content = cwms.get_blob(office_id=TEST_OFFICE, blob_id=TEST_BLOB_ID)
111+
content = blobs.get_blob(office_id=TEST_OFFICE, blob_id=TEST_BLOB_ID)
87112
assert TEST_TEXT in content
88113
assert len(content) >= len(TEST_TEXT)
89114

@@ -97,7 +122,7 @@ def test_update_blob():
97122
"media-type-id": TEST_MEDIA_TYPE,
98123
"value": TEST_TEXT_UPDATED,
99124
}
100-
cwms.update_blob(update, fail_if_not_exists=True)
125+
blobs.update_blob(update, fail_if_not_exists=True)
101126

102127
# Confirm updated metadata
103128
row = _find_blob_row(TEST_OFFICE, TEST_BLOB_UPDATED_ID)
@@ -106,5 +131,5 @@ def test_update_blob():
106131
assert TEST_DESC_UPDATED in str(row["description"])
107132

108133
# Verify new content
109-
content = cwms.get_blob(office_id=TEST_OFFICE, blob_id=TEST_BLOB_UPDATED_ID)
134+
content = blobs.get_blob(office_id=TEST_OFFICE, blob_id=TEST_BLOB_UPDATED_ID)
110135
assert TEST_TEXT_UPDATED in content

tests/cda/resources/blob_test.xlsx

8.66 KB
Binary file not shown.

0 commit comments

Comments
 (0)