From dbfd8f313f5e8b7db09761651829357297e58e08 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Wed, 10 Jun 2026 09:09:24 +0200 Subject: [PATCH] close e2e gaps; require OTP 27+ - CI: integration job now gates the build (was continue-on-error) - add e2e coverage for the multipart open_doc/stream_doc path (the most reworked hackney 4.x code, previously untested end-to-end) - drop the OTP-version crypto shim: require OTP 27+ and always use crypto:mac/4 (removes the platform_define that broke on OTP 30) - CHANGELOG: note OTP 27+ requirement and e2e testing --- .github/workflows/test.yml | 4 +- CHANGELOG.md | 11 +++++- rebar.config | 3 +- src/couchbeam_util.erl | 6 +-- test/couchbeam_integration_SUITE.erl | 58 +++++++++++++++++++++++++++- 5 files changed, 68 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 23b66ac..ef485d4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -120,12 +120,10 @@ jobs: rebar3 xref rebar3 as test eunit - # Optional integration tests with real CouchDB + # End-to-end tests against a real CouchDB integration: name: Integration Tests (CouchDB) runs-on: ubuntu-22.04 - # This job is optional - don't block CI if it fails - continue-on-error: true services: couchdb: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b471f9..e3c00c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,15 @@ All notable changes to this project will be documented in this file. ### Compatibility -- Supports OTP 27, 28 and 29. -- CI now also runs on OTP 29. +- Requires OTP 27+. Dropped the OTP-version crypto shim and now always use + `crypto:mac/4`. +- CI runs on OTP 27, 28 and 29. + +### Testing + +- End-to-end tests run the full client against a real CouchDB. `make e2e` + starts CouchDB in Docker, runs the suite, and tears it down. CI runs all + suite groups and they now gate the build. ### Dependencies diff --git a/rebar.config b/rebar.config index a694c5c..731f7c8 100644 --- a/rebar.config +++ b/rebar.config @@ -1,8 +1,7 @@ %%-*- mode: erlang -*- -{erl_opts, [debug_info, - {platform_define, "^(2[3-9])", 'USE_CRYPTO_MAC'}]}. +{erl_opts, [debug_info]}. %% oauth is an optional dependency - ignore xref warnings for it {xref_ignores, [{oauth, header_params_encode, 1}, {oauth, sign, 6}]}. diff --git a/src/couchbeam_util.erl b/src/couchbeam_util.erl index 7fd4642..a623a17 100644 --- a/src/couchbeam_util.erl +++ b/src/couchbeam_util.erl @@ -285,10 +285,6 @@ hgv(N,L) -> proxy_token(Secret,UserName) -> hackney_bstr:to_hex(hmac(sha, Secret, UserName)). --ifdef(USE_CRYPTO_MAC). +%% couchbeam requires OTP 27+, where crypto:mac/4 is always available. hmac(Alg, Key, Data) -> crypto:mac(hmac, Alg, Key, Data). --else. -hmac(Alg, Key, Data) -> - crypto:hmac(Alg, Key, Data). --endif. diff --git a/test/couchbeam_integration_SUITE.erl b/test/couchbeam_integration_SUITE.erl index f110e4c..2a11ab3 100644 --- a/test/couchbeam_integration_SUITE.erl +++ b/test/couchbeam_integration_SUITE.erl @@ -46,7 +46,8 @@ -export([doc_with_attachment/1, multiple_attachments/1, large_attachment/1, - attachment_streaming/1]). + attachment_streaming/1, + doc_multipart_stream/1]). %% Test cases - Views -export([all_docs/1, @@ -162,7 +163,8 @@ groups() -> doc_with_attachment, multiple_attachments, large_attachment, - attachment_streaming + attachment_streaming, + doc_multipart_stream ]}, {view_ops, [sequence], [ all_docs, @@ -807,6 +809,58 @@ collect_stream(Ref, Acc) -> ct:fail("Stream error: ~p", [Reason]) end. +%% Test reading a document and its attachments as a multipart stream. +doc_multipart_stream(Config) -> + Db = ?config(db, Config), + + %% Create a document with two attachments + Doc = #{<<"_id">> => <<"mp_stream">>, <<"type">> => <<"test">>}, + {ok, DocSaved} = couchbeam:save_doc(Db, Doc), + Rev0 = maps:get(<<"_rev">>, DocSaved), + %% Use a non-compressible content type so CouchDB returns the bytes + %% verbatim in the multipart response (text/* attachments are gzipped). + {ok, Att1} = couchbeam:put_attachment(Db, <<"mp_stream">>, <<"a.bin">>, + <<"alpha">>, + [{rev, Rev0}, + {content_type, <<"application/octet-stream">>}]), + Rev1 = maps:get(<<"rev">>, Att1), + {ok, _} = couchbeam:put_attachment(Db, <<"mp_stream">>, <<"b.bin">>, + <<"bravo">>, + [{rev, Rev1}, + {content_type, <<"application/octet-stream">>}]), + + %% Open it as a multipart stream (attachment bodies included) + {ok, {multipart, State}} = couchbeam:open_doc(Db, <<"mp_stream">>, + [{"attachments", true}]), + + {DocOut, AttsOut} = collect_multipart(State, undefined, <<>>, #{}), + + <<"mp_stream">> = maps:get(<<"_id">>, DocOut), + true = maps:is_key(<<"_attachments">>, DocOut), + <<"alpha">> = maps:get(<<"a.bin">>, AttsOut), + <<"bravo">> = maps:get(<<"b.bin">>, AttsOut), + + ct:pal("Multipart doc stream test passed"), + ok. + +%% Drive couchbeam:stream_doc/1 to completion, collecting the document and +%% each attachment's bytes. +collect_multipart(State, Doc, CurBuf, Atts) -> + case couchbeam:stream_doc(State) of + {doc, NewDoc, NState} -> + collect_multipart(NState, NewDoc, CurBuf, Atts); + {att, _Name, NState} -> + collect_multipart(NState, Doc, <<>>, Atts); + {att_body, _Name, Chunk, NState} -> + collect_multipart(NState, Doc, <>, Atts); + {att_eof, Name, NState} -> + collect_multipart(NState, Doc, <<>>, Atts#{Name => CurBuf}); + eof -> + {Doc, Atts}; + {error, Reason} -> + ct:fail("Multipart stream error: ~p", [Reason]) + end. + %%==================================================================== %% View tests %%====================================================================