diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:49:48 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:49:48 -0300 |
| commit | f979d07d23098b96e758b3feb555cd06d85d9b2d (patch) | |
| tree | 47298197a17a3d6c438a1f4cc7cb61afd390cf05 /tests | |
| download | cobol-c86aef13c0e8995f1c13dd91c4116be40ced3c18.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/cepis-tests.cob | 761 | ||||
| -rw-r--r-- | tests/server.py | 254 |
2 files changed, 1015 insertions, 0 deletions
diff --git a/tests/cepis-tests.cob b/tests/cepis-tests.cob new file mode 100644 index 0000000..4bc1a11 --- /dev/null +++ b/tests/cepis-tests.cob @@ -0,0 +1,761 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. CEPIS-TESTS. + +*> --------------------------------------------------------------------- +*> The suite. Run it through tests/server.py, which binds a loopback +*> port, exports CEPIS_TEST_URL and runs this program: +*> +*> python3 tests/server.py ./build/cepis-tests +*> +*> Every test goes through the real transport against that server, so +*> what is exercised is the code that runs in production -- request +*> construction, header handling and error-body reading included. It +*> doubles as the test that CFG-BASE-URL works: without it none of this +*> would reach anything. +*> +*> The process exits non-zero if any check fails. +*> --------------------------------------------------------------------- + +DATA DIVISION. +WORKING-STORAGE SECTION. +COPY "cepis.cpy". +COPY "cepis-http.cpy". + +01 WS-BASE PIC X(200). +01 WS-CHECKS PIC 9(4) VALUE 0. +01 WS-FAILURES PIC 9(4) VALUE 0. +01 WS-LABEL PIC X(56). +01 WS-DETAIL PIC X(200). +01 WS-OK PIC X. +01 WS-TALLY PIC 9(4). +01 WS-I PIC 9(4). +01 WS-COUNT PIC 9(9). +01 WS-COUNT-BEFORE PIC 9(9). +01 WS-COUNT-PATH PIC X(40). +01 WS-NUM8 PIC 9(8). +01 WS-T0 PIC S9(18) COMP-5. +01 WS-T1 PIC S9(18) COMP-5. +01 WS-ELAPSED PIC S9(18). +01 WS-SAVED-ATTEMPTS PIC 9(4). +01 WS-SAVED-RETRY PIC S9(9). + +01 WS-GOOD-TABLE. + 05 FILLER PIC X(20) VALUE "01001000". + 05 FILLER PIC X(20) VALUE "01001-000". + 05 FILLER PIC X(20) VALUE " 01001-000 ". + 05 FILLER PIC X(20) VALUE "01001.000". +01 WS-GOODS REDEFINES WS-GOOD-TABLE. + 05 WS-GOOD OCCURS 4 TIMES PIC X(20). + +01 WS-BAD-TABLE. + 05 FILLER PIC X(20) VALUE SPACES. + 05 FILLER PIC X(20) VALUE "123". + 05 FILLER PIC X(20) VALUE "0100100a". + 05 FILLER PIC X(20) VALUE "010010000". + 05 FILLER PIC X(20) VALUE "abcdefgh". +01 WS-BADS REDEFINES WS-BAD-TABLE. + 05 WS-BAD OCCURS 5 TIMES PIC X(20). + +PROCEDURE DIVISION. + +MAIN-START. + MOVE SPACES TO WS-BASE + ACCEPT WS-BASE FROM ENVIRONMENT "CEPIS_TEST_URL" + IF WS-BASE = SPACES + DISPLAY "CEPIS_TEST_URL is not set -- run through " + "tests/server.py" + MOVE 2 TO RETURN-CODE + STOP RUN + END-IF + DISPLAY "cepis 0.1.0 test suite against " FUNCTION TRIM (WS-BASE) + DISPLAY " " + + PERFORM T-NORMALIZE-ACCEPTS + PERFORM T-NORMALIZE-REJECTS + PERFORM T-LOOKUP-SUCCESS + PERFORM T-GEO-PRECISE + PERFORM T-GEO-CITY-WIDE + PERFORM T-GEO-MALFORMED + PERFORM T-UNKNOWN-FIELDS + PERFORM T-LONG-VALUE-FLAGGED + PERFORM T-NOT-FOUND + PERFORM T-NOT-FOUND-NOT-RETRIED + PERFORM T-INVALID-NEVER-LEAVES + PERFORM T-API-400 + PERFORM T-NOT-JSON + PERFORM T-RETRY-THEN-SUCCESS + PERFORM T-RETRY-EXHAUSTED + PERFORM T-ONE-ATTEMPT-NO-RETRY + PERFORM T-RATE-LIMITED + PERFORM T-RETRY-AFTER-CAPPED + PERFORM T-RETRY-AFTER-HTTP-DATE + PERFORM T-NETWORK-ERROR + PERFORM T-TOTAL-TIMEOUT + PERFORM T-BATCH-ORDER + PERFORM T-BATCH-QUOTING + PERFORM T-BATCH-MALFORMED-ENTRY + PERFORM T-BATCH-EMPTY + PERFORM T-BATCH-TRUNCATED + PERFORM T-BATCH-CHUNKING + PERFORM T-BATCH-OVER-CAPACITY + PERFORM T-HEALTH + PERFORM T-HEALTH-DEGRADED + PERFORM T-TRAILING-SLASH + PERFORM T-DEFAULT-USER-AGENT + + DISPLAY " " + DISPLAY WS-CHECKS " checks, " WS-FAILURES " failed" + IF WS-FAILURES > 0 + MOVE 1 TO RETURN-CODE + ELSE + MOVE 0 TO RETURN-CODE + END-IF + STOP RUN. + +*> =================================================================== +*> Fixtures +*> =================================================================== +FRESH-CONFIG. + CALL "CEPIS-DEFAULT-CONFIG" USING CEPIS-CONFIG + MOVE WS-BASE TO CFG-BASE-URL + MOVE 5000 TO CFG-TIMEOUT-MS + MOVE 3 TO CFG-MAX-ATTEMPTS + MOVE 1 TO CFG-BACKOFF-BASE-MS + MOVE 10 TO CFG-BACKOFF-MAX-MS + MOVE 0 TO CFG-TOTAL-TIMEOUT-MS. + +CHECK. + ADD 1 TO WS-CHECKS + IF WS-OK = "Y" + DISPLAY "ok " WS-LABEL + ELSE + ADD 1 TO WS-FAILURES + DISPLAY "FAIL " WS-LABEL " -- " FUNCTION TRIM (WS-DETAIL) + END-IF + MOVE SPACES TO WS-DETAIL + MOVE "N" TO WS-OK. + +*> The fake server counts requests per path; that is how a 404 not being +*> retried, or 501 CEPs becoming two requests, is proved rather than +*> assumed. +COUNT-FOR. + PERFORM FRESH-CONFIG + MOVE 1 TO CFG-MAX-ATTEMPTS + MOVE SPACES TO HTTPQ-PATH + STRING "/count?path=" FUNCTION TRIM (WS-COUNT-PATH) + DELIMITED BY SIZE INTO HTTPQ-PATH + END-STRING + COMPUTE HTTPQ-PATH-LEN = + 12 + FUNCTION LENGTH (FUNCTION TRIM (WS-COUNT-PATH)) + MOVE "N" TO HTTPQ-ALLOW-503 + MOVE 0 TO WS-COUNT + CALL "CEPIS-REQUEST" USING CEPIS-CONFIG CEPIS-HTTP-REQUEST + CEPIS-HTTP-RESPONSE CEPIS-STATUS + END-CALL + IF CEPIS-OK AND HTTP-BODY-LEN > 0 + COMPUTE WS-COUNT = + FUNCTION NUMVAL (HTTP-BODY (1:HTTP-BODY-LEN)) + END-IF. + +*> =================================================================== +*> Local validation +*> =================================================================== +T-NORMALIZE-ACCEPTS. + PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 4 + MOVE WS-GOOD (WS-I) TO CEPIS-CEP-INPUT + CALL "CEPIS-NORMALIZE-CEP" + USING CEPIS-CEP-INPUT CEPIS-CEP CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK AND CEPIS-CEP = "01001000" + MOVE "Y" TO WS-OK + END-IF + MOVE SPACES TO WS-LABEL + STRING "normalize accepts " WS-GOOD (WS-I) + DELIMITED BY SIZE INTO WS-LABEL + END-STRING + MOVE CEPIS-CEP TO WS-DETAIL + PERFORM CHECK + END-PERFORM. + +T-NORMALIZE-REJECTS. + PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 5 + MOVE WS-BAD (WS-I) TO CEPIS-CEP-INPUT + CALL "CEPIS-NORMALIZE-CEP" + USING CEPIS-CEP-INPUT CEPIS-CEP CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-INVALID-CEP AND CEPIS-CEP = SPACES + MOVE "Y" TO WS-OK + END-IF + MOVE SPACES TO WS-LABEL + STRING "normalize rejects '" WS-BAD (WS-I) "'" + DELIMITED BY SIZE INTO WS-LABEL + END-STRING + MOVE CEPIS-STATUS-MESSAGE TO WS-DETAIL + PERFORM CHECK + END-PERFORM. + +*> =================================================================== +*> Single lookup +*> =================================================================== +T-LOOKUP-SUCCESS. + PERFORM FRESH-CONFIG + MOVE "01001-000" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK + AND ADDR-LOCALIDADE = "São Paulo" + AND ADDR-UF = "SP" + AND ADDR-LOGRADOURO = "Praça da Sé" + AND ADDR-IBGE = "3550308" + AND ADDR-CEP-MASKED = "01001-000" + AND ADDR-GEO-ABSENT + AND ADDR-NOT-TRUNCATED + MOVE "Y" TO WS-OK + END-IF + MOVE "lookup returns the address, masked cep, no geo" TO WS-LABEL + MOVE CEPIS-STATUS-MESSAGE TO WS-DETAIL + PERFORM CHECK. + +T-GEO-PRECISE. + PERFORM FRESH-CONFIG + MOVE "01310100" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK AND ADDR-GEO-KNOWN AND ADDR-GEO-PRECISE + AND ADDR-GEO-RAIO-M = 120 + AND ADDR-GEO-LAT < -23.5 AND ADDR-GEO-LAT > -23.6 + AND ADDR-GEO-LON < -46.6 AND ADDR-GEO-LON > -46.7 + MOVE "Y" TO WS-OK + END-IF + MOVE "geo present is read and reports precisao logradouro" + TO WS-LABEL + PERFORM CHECK. + +T-GEO-CITY-WIDE. + PERFORM FRESH-CONFIG + MOVE "01001001" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK AND ADDR-GEO-KNOWN AND ADDR-GEO-CITY-WIDE + AND NOT ADDR-GEO-PRECISE AND ADDR-GEO-RAIO-M = 22000 + MOVE "Y" TO WS-OK + END-IF + MOVE "a municipality centroid is not precise" TO WS-LABEL + PERFORM CHECK. + +T-GEO-MALFORMED. + PERFORM FRESH-CONFIG + MOVE "01001002" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK AND ADDR-GEO-ABSENT + AND ADDR-LOCALIDADE = "São Paulo" + MOVE "Y" TO WS-OK + END-IF + MOVE "a malformed geo does not lose the address" TO WS-LABEL + PERFORM CHECK. + +T-UNKNOWN-FIELDS. + PERFORM FRESH-CONFIG + MOVE "01001003" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK AND ADDR-LOCALIDADE = "São Paulo" + AND ADDR-GEO-ABSENT + MOVE "Y" TO WS-OK + END-IF + MOVE "fields the client does not know are ignored" TO WS-LABEL + MOVE CEPIS-STATUS-MESSAGE TO WS-DETAIL + PERFORM CHECK. + +T-LONG-VALUE-FLAGGED. + PERFORM FRESH-CONFIG + MOVE "01001010" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK AND ADDR-WAS-TRUNCATED + MOVE "Y" TO WS-OK + END-IF + MOVE "a value past the field width sets ADDR-TRUNCATED" + TO WS-LABEL + PERFORM CHECK. + +T-NOT-FOUND. + PERFORM FRESH-CONFIG + MOVE "99999999" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-NOT-FOUND AND ADDR-LOCALIDADE = SPACES + MOVE "Y" TO WS-OK + END-IF + MOVE "a missing CEP is CEPIS-NOT-FOUND" TO WS-LABEL + MOVE CEPIS-STATUS-MESSAGE TO WS-DETAIL + PERFORM CHECK. + +T-NOT-FOUND-NOT-RETRIED. + MOVE "/99999999" TO WS-COUNT-PATH + PERFORM COUNT-FOR + MOVE "N" TO WS-OK + IF WS-COUNT = 1 + MOVE "Y" TO WS-OK + END-IF + MOVE "404 is definitive and is not retried" TO WS-LABEL + MOVE SPACES TO WS-DETAIL + STRING "requests: " WS-COUNT + DELIMITED BY SIZE INTO WS-DETAIL + END-STRING + PERFORM CHECK. + +T-INVALID-NEVER-LEAVES. + PERFORM FRESH-CONFIG + MOVE "123" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-INVALID-CEP + MOVE "Y" TO WS-OK + END-IF + MOVE "/123" TO WS-COUNT-PATH + PERFORM COUNT-FOR + IF WS-COUNT NOT = 0 + MOVE "N" TO WS-OK + END-IF + MOVE "a malformed CEP never leaves the machine" TO WS-LABEL + PERFORM CHECK. + +T-API-400. + PERFORM FRESH-CONFIG + MOVE "00000000" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE 0 TO WS-TALLY + INSPECT CEPIS-STATUS-MESSAGE + TALLYING WS-TALLY FOR ALL "rejected by the API" + MOVE "N" TO WS-OK + IF CEPIS-INVALID-CEP AND WS-TALLY = 1 + MOVE "Y" TO WS-OK + END-IF + MOVE "an API 400 carries the API's own message" TO WS-LABEL + MOVE CEPIS-STATUS-MESSAGE TO WS-DETAIL + PERFORM CHECK. + +T-NOT-JSON. + PERFORM FRESH-CONFIG + MOVE "01001004" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-INVALID-RESPONSE + MOVE "Y" TO WS-OK + END-IF + MOVE "a body that is not JSON is CEPIS-INVALID-RESPONSE" + TO WS-LABEL + MOVE CEPIS-STATUS-MESSAGE TO WS-DETAIL + PERFORM CHECK. + +*> =================================================================== +*> Retrying +*> =================================================================== +T-RETRY-THEN-SUCCESS. + PERFORM FRESH-CONFIG + MOVE "01001005" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK AND ADDR-LOCALIDADE = "São Paulo" + MOVE "Y" TO WS-OK + END-IF + MOVE "/01001005" TO WS-COUNT-PATH + PERFORM COUNT-FOR + IF WS-COUNT NOT = 2 + MOVE "N" TO WS-OK + END-IF + MOVE "500 is retried and the second attempt succeeds" TO WS-LABEL + MOVE SPACES TO WS-DETAIL + STRING "requests: " WS-COUNT + DELIMITED BY SIZE INTO WS-DETAIL + END-STRING + PERFORM CHECK. + +T-RETRY-EXHAUSTED. + PERFORM FRESH-CONFIG + MOVE "01001006" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-SERVER-ERROR AND CEPIS-STATUS-HTTP = 503 + AND CEPIS-STATUS-ATTEMPTS = 3 + MOVE "Y" TO WS-OK + END-IF + MOVE "/01001006" TO WS-COUNT-PATH + PERFORM COUNT-FOR + IF WS-COUNT NOT = 3 + MOVE "N" TO WS-OK + END-IF + MOVE "three attempts, then CEPIS-SERVER-ERROR" TO WS-LABEL + MOVE SPACES TO WS-DETAIL + STRING "requests: " WS-COUNT + DELIMITED BY SIZE INTO WS-DETAIL + END-STRING + PERFORM CHECK. + +T-ONE-ATTEMPT-NO-RETRY. + PERFORM FRESH-CONFIG + MOVE 1 TO CFG-MAX-ATTEMPTS + MOVE "01001012" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-SERVER-ERROR + MOVE "Y" TO WS-OK + END-IF + MOVE "/01001012" TO WS-COUNT-PATH + PERFORM COUNT-FOR + IF WS-COUNT NOT = 1 + MOVE "N" TO WS-OK + END-IF + MOVE "CFG-MAX-ATTEMPTS of 1 disables retrying" TO WS-LABEL + PERFORM CHECK. + +T-RATE-LIMITED. + PERFORM FRESH-CONFIG + MOVE "01001007" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-RATE-LIMITED AND CEPIS-STATUS-RETRY-AFTER-MS = 7000 + MOVE "Y" TO WS-OK + END-IF + MOVE "429 exposes Retry-After in seconds" TO WS-LABEL + MOVE SPACES TO WS-DETAIL + STRING "retry-after ms: " CEPIS-STATUS-RETRY-AFTER-MS + DELIMITED BY SIZE INTO WS-DETAIL + END-STRING + PERFORM CHECK. + +T-RETRY-AFTER-CAPPED. + PERFORM FRESH-CONFIG + MOVE "01001008" TO CEPIS-CEP-INPUT + CALL "cepis_monotonic_ms" USING WS-T0 + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + CALL "cepis_monotonic_ms" USING WS-T1 + COMPUTE WS-ELAPSED = WS-T1 - WS-T0 + MOVE "N" TO WS-OK + IF CEPIS-RATE-LIMITED AND WS-ELAPSED < 3000 + MOVE "Y" TO WS-OK + END-IF + MOVE "an absurd Retry-After is capped at the backoff maximum" + TO WS-LABEL + MOVE SPACES TO WS-DETAIL + STRING "elapsed ms: " WS-ELAPSED + DELIMITED BY SIZE INTO WS-DETAIL + END-STRING + PERFORM CHECK. + +T-RETRY-AFTER-HTTP-DATE. + PERFORM FRESH-CONFIG + MOVE "01001009" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-RATE-LIMITED + AND CEPIS-STATUS-RETRY-AFTER-MS >= 1000 + AND CEPIS-STATUS-RETRY-AFTER-MS <= 8000 + MOVE "Y" TO WS-OK + END-IF + MOVE "Retry-After is also read as an HTTP date" TO WS-LABEL + MOVE SPACES TO WS-DETAIL + STRING "retry-after ms: " CEPIS-STATUS-RETRY-AFTER-MS + DELIMITED BY SIZE INTO WS-DETAIL + END-STRING + PERFORM CHECK. + +T-NETWORK-ERROR. + PERFORM FRESH-CONFIG + MOVE "http://127.0.0.1:1" TO CFG-BASE-URL + MOVE 2 TO CFG-MAX-ATTEMPTS + MOVE 1000 TO CFG-TIMEOUT-MS + MOVE "01001000" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-NETWORK-ERROR AND CEPIS-STATUS-ATTEMPTS = 2 + MOVE "Y" TO WS-OK + END-IF + MOVE "a refused connection is CEPIS-NETWORK-ERROR" TO WS-LABEL + MOVE CEPIS-STATUS-MESSAGE TO WS-DETAIL + PERFORM CHECK. + +T-TOTAL-TIMEOUT. + PERFORM FRESH-CONFIG + MOVE 50 TO CFG-MAX-ATTEMPTS + MOVE 50 TO CFG-BACKOFF-BASE-MS + MOVE 50 TO CFG-BACKOFF-MAX-MS + MOVE 300 TO CFG-TOTAL-TIMEOUT-MS + MOVE "01001011" TO CEPIS-CEP-INPUT + CALL "cepis_monotonic_ms" USING WS-T0 + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + CALL "cepis_monotonic_ms" USING WS-T1 + COMPUTE WS-ELAPSED = WS-T1 - WS-T0 + MOVE CEPIS-STATUS-ATTEMPTS TO WS-SAVED-ATTEMPTS + MOVE "N" TO WS-OK + IF CEPIS-SERVER-ERROR AND WS-ELAPSED < 3000 + AND WS-SAVED-ATTEMPTS < 50 + MOVE "Y" TO WS-OK + END-IF + MOVE "CFG-TOTAL-TIMEOUT-MS bounds the whole call" TO WS-LABEL + MOVE SPACES TO WS-DETAIL + STRING "elapsed ms: " WS-ELAPSED + " attempts: " WS-SAVED-ATTEMPTS + DELIMITED BY SIZE INTO WS-DETAIL + END-STRING + PERFORM CHECK. + +*> =================================================================== +*> Batch +*> =================================================================== +T-BATCH-ORDER. + PERFORM FRESH-CONFIG + MOVE 3 TO BATCH-COUNT + MOVE "01001000" TO BATCH-REQUESTED (1) + MOVE "99999999" TO BATCH-REQUESTED (2) + MOVE "20040020" TO BATCH-REQUESTED (3) + CALL "CEPIS-LOOKUP-MANY" USING CEPIS-CONFIG CEPIS-BATCH + CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK + AND BATCH-ENTRY-FOUND (1) + AND BAT-LOCALIDADE (1) = "São Paulo" + AND BATCH-ENTRY-MISSING (2) + AND BATCH-ENTRY-FOUND (3) + AND BAT-UF (3) = "RJ" + AND BAT-GEO-ABSENT (1) + MOVE "Y" TO WS-OK + END-IF + MOVE "a missing CEP keeps its slot and nothing shifts" TO WS-LABEL + MOVE CEPIS-STATUS-MESSAGE TO WS-DETAIL + PERFORM CHECK. + +T-BATCH-QUOTING. + PERFORM FRESH-CONFIG + MOVE 1 TO BATCH-COUNT + MOVE "01001111" TO BATCH-REQUESTED (1) + CALL "CEPIS-LOOKUP-MANY" USING CEPIS-CONFIG CEPIS-BATCH + CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK AND BATCH-ENTRY-FOUND (1) + AND BAT-LOGRADOURO (1) = "Rua Um, Dois" + AND BAT-COMPLEMENTO (1) = 'aspas "aqui"' + MOVE "Y" TO WS-OK + END-IF + MOVE "quoted CSV fields with commas and quotes are parsed" + TO WS-LABEL + MOVE BAT-LOGRADOURO (1) TO WS-DETAIL + PERFORM CHECK. + +T-BATCH-MALFORMED-ENTRY. + PERFORM FRESH-CONFIG + MOVE 3 TO BATCH-COUNT + MOVE "01001000" TO BATCH-REQUESTED (1) + MOVE "nonsense" TO BATCH-REQUESTED (2) + MOVE "20040020" TO BATCH-REQUESTED (3) + CALL "CEPIS-LOOKUP-MANY" USING CEPIS-CONFIG CEPIS-BATCH + CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK + AND BATCH-ENTRY-FOUND (1) + AND BATCH-ENTRY-MISSING (2) + AND BATCH-ENTRY-FOUND (3) + AND BAT-UF (3) = "RJ" + MOVE "Y" TO WS-OK + END-IF + MOVE "a malformed CEP is missing, not an error" TO WS-LABEL + PERFORM CHECK. + +T-BATCH-EMPTY. + MOVE "/csv" TO WS-COUNT-PATH + PERFORM COUNT-FOR + MOVE WS-COUNT TO WS-COUNT-BEFORE + PERFORM FRESH-CONFIG + MOVE 0 TO BATCH-COUNT + CALL "CEPIS-LOOKUP-MANY" USING CEPIS-CONFIG CEPIS-BATCH + CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK + MOVE "Y" TO WS-OK + END-IF + MOVE "/csv" TO WS-COUNT-PATH + PERFORM COUNT-FOR + IF WS-COUNT NOT = WS-COUNT-BEFORE + MOVE "N" TO WS-OK + END-IF + MOVE "an empty batch makes no request" TO WS-LABEL + PERFORM CHECK. + +T-BATCH-TRUNCATED. + PERFORM FRESH-CONFIG + MOVE 2 TO BATCH-COUNT + MOVE "01001000" TO BATCH-REQUESTED (1) + MOVE "00000099" TO BATCH-REQUESTED (2) + CALL "CEPIS-LOOKUP-MANY" USING CEPIS-CONFIG CEPIS-BATCH + CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK AND BATCH-ENTRY-FOUND (1) + AND BATCH-ENTRY-MISSING (2) + MOVE "Y" TO WS-OK + END-IF + MOVE "a short response pads rather than shifts" TO WS-LABEL + PERFORM CHECK. + +T-BATCH-CHUNKING. + MOVE "/csv" TO WS-COUNT-PATH + PERFORM COUNT-FOR + MOVE WS-COUNT TO WS-COUNT-BEFORE + PERFORM FRESH-CONFIG + MOVE 501 TO BATCH-COUNT + PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 501 + COMPUTE WS-NUM8 = 70000000 + WS-I + MOVE WS-NUM8 TO BATCH-REQUESTED (WS-I) + END-PERFORM + CALL "CEPIS-LOOKUP-MANY" USING CEPIS-CONFIG CEPIS-BATCH + CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK AND BATCH-ENTRY-FOUND (1) + AND BATCH-ENTRY-FOUND (500) + AND BATCH-ENTRY-FOUND (501) + AND BAT-LOCALIDADE (501) = "Cidade Sintetica" + MOVE "Y" TO WS-OK + END-IF + MOVE "/csv" TO WS-COUNT-PATH + PERFORM COUNT-FOR + COMPUTE WS-COUNT = WS-COUNT - WS-COUNT-BEFORE + IF WS-COUNT NOT = 2 + MOVE "N" TO WS-OK + END-IF + MOVE "501 CEPs become two requests, not one 400" TO WS-LABEL + MOVE SPACES TO WS-DETAIL + STRING "requests: " WS-COUNT " status: " CEPIS-STATUS-MESSAGE + DELIMITED BY SIZE INTO WS-DETAIL + END-STRING + PERFORM CHECK. + +T-BATCH-OVER-CAPACITY. + PERFORM FRESH-CONFIG + MOVE 1001 TO BATCH-COUNT + CALL "CEPIS-LOOKUP-MANY" USING CEPIS-CONFIG CEPIS-BATCH + CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-INVALID-CEP + MOVE "Y" TO WS-OK + END-IF + MOVE 0 TO BATCH-COUNT + MOVE "a BATCH-COUNT past the table is refused, not truncated" + TO WS-LABEL + MOVE CEPIS-STATUS-MESSAGE TO WS-DETAIL + PERFORM CHECK. + +*> =================================================================== +*> Health and configuration +*> =================================================================== +T-HEALTH. + PERFORM FRESH-CONFIG + CALL "CEPIS-HEALTH" USING CEPIS-CONFIG CEPIS-HEALTH CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK AND HEALTH-OK AND HEALTH-DNE = "26071" + AND HEALTH-CEPS = 1596168 AND HEALTH-GEO = 894489 + AND HEALTH-BASE-GERADA-EM = "2026-07-24T03:48:28Z" + MOVE "Y" TO WS-OK + END-IF + MOVE "health reports the live database" TO WS-LABEL + MOVE CEPIS-STATUS-MESSAGE TO WS-DETAIL + PERFORM CHECK. + +T-HEALTH-DEGRADED. + PERFORM FRESH-CONFIG + MOVE SPACES TO CFG-BASE-URL + STRING FUNCTION TRIM (WS-BASE) "/degraded" + DELIMITED BY SIZE INTO CFG-BASE-URL + END-STRING + CALL "CEPIS-HEALTH" USING CEPIS-CONFIG CEPIS-HEALTH CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK AND NOT HEALTH-OK + AND HEALTH-STATUS = "degradado" + AND CEPIS-STATUS-ATTEMPTS = 1 + MOVE "Y" TO WS-OK + END-IF + MOVE "a degraded API is diagnosed, not retried" TO WS-LABEL + MOVE HEALTH-STATUS TO WS-DETAIL + PERFORM CHECK. + +T-TRAILING-SLASH. + PERFORM FRESH-CONFIG + MOVE SPACES TO CFG-BASE-URL + STRING FUNCTION TRIM (WS-BASE) "/" + DELIMITED BY SIZE INTO CFG-BASE-URL + END-STRING + MOVE "01001000" TO CEPIS-CEP-INPUT + CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT + CEPIS-ADDRESS CEPIS-STATUS + END-CALL + MOVE "N" TO WS-OK + IF CEPIS-OK AND ADDR-UF = "SP" + MOVE "Y" TO WS-OK + END-IF + MOVE "a trailing slash on the base URL is dropped" TO WS-LABEL + MOVE CEPIS-STATUS-MESSAGE TO WS-DETAIL + PERFORM CHECK. + +T-DEFAULT-USER-AGENT. + CALL "CEPIS-DEFAULT-CONFIG" USING CEPIS-CONFIG + MOVE 0 TO WS-TALLY + INSPECT CFG-USER-AGENT TALLYING WS-TALLY FOR ALL "cepis-cobol/0.1.0" + MOVE "N" TO WS-OK + IF WS-TALLY = 1 AND CFG-BASE-URL = "https://cep.is" + AND CFG-MAX-ATTEMPTS = 3 AND CFG-BACKOFF-BASE-MS = 500 + AND CFG-BACKOFF-MAX-MS = 8000 + MOVE "Y" TO WS-OK + END-IF + MOVE "the defaults identify the library and its version" + TO WS-LABEL + MOVE CFG-USER-AGENT TO WS-DETAIL + PERFORM CHECK. + +END PROGRAM CEPIS-TESTS. diff --git a/tests/server.py b/tests/server.py new file mode 100644 index 0000000..46185a0 --- /dev/null +++ b/tests/server.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +"""Fake cep.is for the COBOL test suite. + +Binds a loopback port, points CEPIS_TEST_URL at it, runs the command given +on the argv and exits with that command's status. Standing an HTTP server up +from COBOL is not practical; every route below is a fixture the driver in +`cepis-tests.cob` asserts against, and no test touches the network. + + python3 tests/server.py ./build/cepis-tests + +The counters behind `/count` are what let the driver prove that a 404 is not +retried, that a 500 is, and that 501 CEPs become two batch requests. +""" + +import email.utils +import json +import os +import subprocess +import sys +import threading +import time +import urllib.parse +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +SE = { + "cep": "01001000", + "logradouro": "Praça da Sé", + "complemento": "lado ímpar", + "unidade": "", + "bairro": "Sé", + "localidade": "São Paulo", + "uf": "SP", + "estado": "São Paulo", + "regiao": "Sudeste", + "ibge": "3550308", + "gia": "1004", + "ddd": "11", + "siafi": "7107", +} + +CSV_COLUMNS = [ + "cep", + "logradouro", + "complemento", + "unidade", + "bairro", + "localidade", + "uf", + "estado", + "regiao", + "ibge", + "gia", + "ddd", + "siafi", +] + +CSV_ROWS = { + "01001000": [ + "Praça da Sé", "lado ímpar", "", "Sé", "São Paulo", "SP", + "São Paulo", "Sudeste", "3550308", "1004", "11", "7107", + ], + "20040020": [ + "Avenida Rio Branco", "", "", "Centro", "Rio de Janeiro", "RJ", + "Rio de Janeiro", "Sudeste", "3304557", "", "21", "6001", + ], + "01001111": [ + "Rua Um, Dois", 'aspas "aqui"', "", "Centro", "São Paulo", "SP", + "São Paulo", "Sudeste", "3550308", "", "11", "7107", + ], +} + +SYNTHETIC = [ + "Rua Sintetica", "", "", "Centro", "Cidade Sintetica", "SP", + "São Paulo", "Sudeste", "3550308", "", "11", "7107", +] + +DROPPED_CEP = "00000099" + +LONG_STREET = "Avenida " + ("Muito Comprida " * 20) + +COUNTS = {} +COUNT_LOCK = threading.Lock() + + +def with_geo(precisao, raio): + body = dict(SE) + body["geo"] = { + "lat": -23.567202, + "lon": -46.649659, + "raio_m": raio, + "precisao": precisao, + } + return body + + +def csv_escape(value): + if any(c in value for c in (",", '"', "\n", "\r")): + return '"' + value.replace('"', '""') + '"' + return value + + +def csv_body(ceps): + lines = [",".join(CSV_COLUMNS)] + for cep in ceps: + if cep == DROPPED_CEP: + continue + if cep in CSV_ROWS: + values = [cep] + CSV_ROWS[cep] + elif cep.startswith("7") and len(cep) == 8: + values = [cep] + SYNTHETIC + else: + values = [cep] + [""] * 12 + lines.append(",".join(csv_escape(v) for v in values)) + return "\n".join(lines) + "\n" + + +def json_body(payload): + return json.dumps(payload, ensure_ascii=False) + + +def bump(path): + with COUNT_LOCK: + COUNTS[path] = COUNTS.get(path, 0) + 1 + return COUNTS[path] + + +def route(path, query, hits): + """Return (status, body, headers) for one request.""" + if path == "/01001000": + return 200, json_body(SE), {} + if path == "/01310100": + return 200, json_body(with_geo("logradouro", 120)), {} + if path == "/01001001": + return 200, json_body(with_geo("localidade", 22000)), {} + if path == "/01001002": + body = dict(SE) + body["geo"] = {"lat": "not a number"} + return 200, json_body(body), {} + if path == "/01001003": + body = dict(SE) + body["campo_do_futuro"] = "value" + body["objeto_do_futuro"] = {"a": [1, 2, {"lat": 9}]} + return 200, json_body(body), {} + if path == "/01001004": + return 200, "<html>captive portal</html>", {} + if path == "/01001010": + body = dict(SE) + body["logradouro"] = LONG_STREET + return 200, json_body(body), {} + if path == "/01001005": + if hits == 1: + return 500, json_body({"erro": "interno"}), {} + return 200, json_body(SE), {} + if path in ("/01001006", "/01001011"): + return 503, json_body({"erro": "interno"}), {} + if path == "/01001012": + return 500, json_body({"erro": "interno"}), {} + if path == "/01001007": + return 429, json_body({"erro": "rate"}), {"Retry-After": "7"} + if path == "/01001008": + return 429, json_body({"erro": "rate"}), {"Retry-After": "3600"} + if path == "/01001009": + when = email.utils.formatdate(time.time() + 4, usegmt=True) + return 429, json_body({"erro": "rate"}), {"Retry-After": when} + if path == "/99999999": + return 404, json_body( + {"erro": "cep_nao_encontrado", "mensagem": "gone"} + ), {} + if path == "/00000000": + return 400, json_body( + {"erro": "cep_invalido", "mensagem": "rejected by the API"} + ), {} + if path == "/health": + return 200, json_body( + { + "status": "ok", + "dne": "26071", + "ceps": 1596168, + "geo": 894489, + "base_gerada_em": "2026-07-24T03:48:28Z", + } + ), {} + if path == "/degraded/health": + return 503, json_body( + {"status": "degradado", "dne": "", "ceps": 0, "geo": 0} + ), {} + if path == "/csv": + raw = urllib.parse.parse_qs(query).get("ceps", [""])[0] + ceps = [c for c in raw.split(",") if c] + if len(ceps) > 500: + return 400, json_body( + {"erro": "muitos_ceps", "mensagem": "no máximo 500"} + ), {} + return 200, csv_body(ceps), {} + return 404, json_body({"erro": "cep_nao_encontrado"}), {} + + +class Handler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def do_GET(self): + parsed = urllib.parse.urlsplit(self.path) + path = parsed.path + + if path == "/count": + wanted = urllib.parse.parse_qs(parsed.query).get("path", [""])[0] + with COUNT_LOCK: + total = sum( + n for p, n in COUNTS.items() if p.startswith(wanted) + ) + self.reply(200, str(total), {}) + return + + hits = bump(path) + status, body, headers = route(path, parsed.query, hits) + self.reply(status, body, headers) + + def reply(self, status, body, headers): + data = body.encode("utf-8") + self.send_response(status) + for key, value in headers.items(): + self.send_header(key, value) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def log_message(self, *args): + pass + + +def main(): + if len(sys.argv) < 2: + print("usage: server.py <command> [args...]", file=sys.stderr) + return 2 + + server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + host, port = server.server_address[:2] + thread = threading.Thread( + target=server.serve_forever, kwargs={"poll_interval": 0.01}, daemon=True + ) + thread.start() + + env = dict(os.environ) + env["CEPIS_TEST_URL"] = "http://{}:{}".format(host, port) + try: + return subprocess.call(sys.argv[1:], env=env) + finally: + server.shutdown() + server.server_close() + + +if __name__ == "__main__": + sys.exit(main()) |