<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<title>Blog posts - Toni Vanhala</title>
	<subtitle>Blog about software dev, data engineering, AI, and other stuff</subtitle>
	<link href="https://vanhala.org/posts/feed.xml" rel="self" type="application/atom+xml"/>
    <link href="https://vanhala.org/posts/"/>
	<updated>2025-09-08T00:00:00+00:00</updated>
	<id>https://vanhala.org/posts/feed.xml</id>
	<entry xml:lang="en">
		<title>Local Integration Testing With Pytest</title>
		<published>2025-09-08T00:00:00+00:00</published>
		<updated>2025-09-08T00:00:00+00:00</updated>
        <summary>&lt;p&gt;The traditional testing pyramid encourages us to focus on writing unit tests that are fast and cheap, while avoiding slow and complex integration testing. Real-world experience also shows that poor design and complexity often go hand in hand; I&#x27;ve seen my share of messy and flaky integration test setups. No wonder that people have learned to avoid anything but unit testing.&lt;&#x2F;p&gt;
&lt;p&gt;There is obvious value in verifying how the &lt;em&gt;units&lt;&#x2F;em&gt; of our software &lt;em&gt;integrate&lt;&#x2F;em&gt; to serve use cases. You can&#x27;t simply ignore integration testing, unless you absolutely love solving production incidents under high pressure. Fortunately, there are ways to test business logic and service integrations in a contained and controlled fashion. If we spend some time and care for the kinds of tests we write, we can achieve robust integration testing without the typical overhead and flakiness associated with higher-level tests.&lt;&#x2F;p&gt;</summary>
		<link href="https://vanhala.org/posts/20250908-local-integration-testing/" type="text/html"/>
		<id>https://vanhala.org/posts/20250908-local-integration-testing/</id>
		<content type="html">&lt;p&gt;The traditional testing pyramid encourages us to focus on writing unit tests that are fast and cheap, while avoiding slow and complex integration testing. Real-world experience also shows that poor design and complexity often go hand in hand; I&#x27;ve seen my share of messy and flaky integration test setups. No wonder that people have learned to avoid anything but unit testing.&lt;&#x2F;p&gt;
&lt;p&gt;There is obvious value in verifying how the &lt;em&gt;units&lt;&#x2F;em&gt; of our software &lt;em&gt;integrate&lt;&#x2F;em&gt; to serve use cases. You can&#x27;t simply ignore integration testing, unless you absolutely love solving production incidents under high pressure. Fortunately, there are ways to test business logic and service integrations in a contained and controlled fashion. If we spend some time and care for the kinds of tests we write, we can achieve robust integration testing without the typical overhead and flakiness associated with higher-level tests.&lt;&#x2F;p&gt;
&lt;span id=&quot;continue-reading&quot;&gt;&lt;&#x2F;span&gt;&lt;h2 id=&quot;a-painful-hike-up-the-test-pyramid&quot;&gt;A painful hike up the test pyramid&lt;a class=&quot;zola-anchor&quot; href=&quot;#a-painful-hike-up-the-test-pyramid&quot; aria-label=&quot;Anchor link for: a-painful-hike-up-the-test-pyramid&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The testing pyramid is a good starting point for thinking about different types of tests: unit tests at the bottom, service or integration tests in the middle, and UI or end-to-end (E2E) tests at the top. Unit tests are fast and cheap, while moving upwards in the pyramid makes everything slower and harder to set up and maintain. It makes sense to heed the advice that the low-level unit tests should outnumber the higher level tests by an order of magnitude.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vanhala.org&#x2F;posts&#x2F;20250908-local-integration-testing&#x2F;test_pyramid.jpg&quot; alt=&quot;The test pyramid with unit tests, integration tests, and end-to-end tests&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I once saw the integration test suite fail when a local username exceeded 10 characters, and a dynamically deployed message queue ran out of characters to give. Such experiences don&#x27;t exactly instill confidence in running, let alone setting up and maintaining, tests that depend on anything beyond your immediate code.&lt;&#x2F;p&gt;
&lt;p&gt;Yes, tests involving multiple components, external services, and complex logic can be flaky and non-deterministic. However, you don&#x27;t need to rely on E2E tests alone to verify business logic and service integrations. Many integrations can be contained and verified while avoiding complex setup and maintenance overhead.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-taxonomy-of-integration-tests&quot;&gt;A taxonomy of integration tests&lt;a class=&quot;zola-anchor&quot; href=&quot;#a-taxonomy-of-integration-tests&quot; aria-label=&quot;Anchor link for: a-taxonomy-of-integration-tests&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;In part we suffer from the lack of shared terminology. Integration tests can mean anything from checking a single component&#x27;s interaction with a database to complex tests involving multiple services and shared external dependencies.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s use a loose taxonomy for discussing the different types of integration tests. We&#x27;ll start by looking at unit tests that don&#x27;t really &quot;integrate&quot; anything; moving on to component integration tests, which let components handle their own responsibilities and implementation details; stopping to review API layer tests, which validate contracts and data models at the border; and ending with a discussion of system integrations, which are likely to have some fuzziness and non-determinism.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;unit-testing&quot;&gt;Unit testing&lt;a class=&quot;zola-anchor&quot; href=&quot;#unit-testing&quot; aria-label=&quot;Anchor link for: unit-testing&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;A somewhat circular definition for unit tests goes: they test an &quot;unit&quot; of code. This begs the question of what a unit of code is.&lt;&#x2F;p&gt;
&lt;p&gt;Software is made of modules: functions, classes, programming language modules, and so on. Modules contain related functionality, reducing the context that a developer needs to reason about when working to solve a cohesive piece of a problem. Unit testing happens within the boundaries of one module, where you can focus on testing the programming logic instead of interactions with other modules.&lt;&#x2F;p&gt;
&lt;p&gt;The following unit test from my &lt;a rel=&quot;nofollow noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tonivanhala&#x2F;celery-decipher&quot;&gt;example Celery project&lt;&#x2F;a&gt; verifies that the core logic for ciphering and deciphering text works as intended.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span&gt;celery_decipher.decipher.cipher &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ROT13_CIPHER&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;    cipher,
&lt;&#x2F;span&gt;&lt;span&gt;    decipher,
&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;def &lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;test_decipher&lt;&#x2F;span&gt;&lt;span&gt;():
&lt;&#x2F;span&gt;&lt;span&gt;    text = &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;cat&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;    ciphered = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;cipher&lt;&#x2F;span&gt;&lt;span&gt;(text, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ROT13_CIPHER&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;assert &lt;&#x2F;span&gt;&lt;span&gt;ciphered == &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;png&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;    deciphered = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;decipher&lt;&#x2F;span&gt;&lt;span&gt;(ciphered, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ROT13_CIPHER&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;assert &lt;&#x2F;span&gt;&lt;span&gt;deciphered == &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;cat&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The test uses a data fixture and functions imported from one Python module, which is a strong indicator that the test targets tightly related functionality. Well, either that or your modules are organized poorly. We use the public interface of the module, in this case, functions and their signatures, and avoid testing implementation details. If your unit tests break whenever the internal logic in the module changes, you should rethink the modularization of your code, e.g., ensure that the interface is narrow and doesn&#x27;t leak abstractions.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;component-integration-tests&quot;&gt;Component integration tests&lt;a class=&quot;zola-anchor&quot; href=&quot;#component-integration-tests&quot; aria-label=&quot;Anchor link for: component-integration-tests&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Working within one module is nice and comfortable, but usually doesn&#x27;t cover many interesting use cases. Unless you inherited the one project, where everything was contained within a single &lt;strong&gt;C&lt;&#x2F;strong&gt; language function, causing me to lose my faith in humanity. Barring that, you most likely have some sort of a software architecture, where you need to integrate different components to achieve things like sending and receiving messages, uploading files, and querying and persisting data.&lt;&#x2F;p&gt;
&lt;p&gt;A basic functionality in most applications is to persist data, which can then be transformed and queried. Luckily, there isn&#x27;t much effort in integrating a real database to a test setup, as we can run most of them in containers. I personally try my best to enable most services to run locally, which is &lt;a rel=&quot;nofollow noreferrer&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;ExperiencedDevs&#x2F;comments&#x2F;1m950sh&#x2F;is_it_unreasonable_to_expect_that_most_services&#x2F;&quot;&gt;a sentiment that many experienced developers agree with&lt;&#x2F;a&gt;. This allows the tests to cover more ground instead of using test doubles for their dependencies.&lt;&#x2F;p&gt;
&lt;p&gt;The example project has a Docker Compose setup for running a local PostgreSQL server with a separate test database. This allows running component integration tests, while a connection to the test database is explicitly injected.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span&gt;celery_decipher.decipher.db &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;    get_candidates,
&lt;&#x2F;span&gt;&lt;span&gt;    get_source_text,
&lt;&#x2F;span&gt;&lt;span&gt;    insert_source_text,
&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span&gt;celery_decipher.decipher.solver &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;POPULATION_SIZE&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;    initial_guess,
&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;def &lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;test_initial_guess&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;testdb_cursor&lt;&#x2F;span&gt;&lt;span&gt;):
&lt;&#x2F;span&gt;&lt;span&gt;    text = &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;Smoky smoke test&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;    source_text_id = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;insert_source_text&lt;&#x2F;span&gt;&lt;span&gt;(testdb_cursor, text)
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;initial_guess&lt;&#x2F;span&gt;&lt;span&gt;(testdb_cursor, source_text_id)
&lt;&#x2F;span&gt;&lt;span&gt;    candidates = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;get_candidates&lt;&#x2F;span&gt;&lt;span&gt;(testdb_cursor, source_text_id)
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;assert &lt;&#x2F;span&gt;&lt;span&gt;candidates is not &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;None
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;assert &lt;&#x2F;span&gt;&lt;span style=&quot;color:#96b5b4;&quot;&gt;len&lt;&#x2F;span&gt;&lt;span&gt;(candidates) == &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;POPULATION_SIZE
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This example test uses database layer functions for inserting test data and fetching the results after calling the higher-level function which we are testing. When the modules are deep and their interfaces are narrow, our tests are clean: We don&#x27;t need to care how the higher-level function &lt;code&gt;initial_guess&lt;&#x2F;code&gt; does its magic, while it internally uses the same database module as we do. In fact, we have no business in looking inside &lt;code&gt;initial_guess&lt;&#x2F;code&gt;, for example, poking for state changes within the &lt;code&gt;solver&lt;&#x2F;code&gt; module. We should test the public interface instead, while avoiding assumptions about implementation details. In general, you shouldn&#x27;t create tight couplings in tests, so that you can benefit from loose couplings between modules.&lt;&#x2F;p&gt;
&lt;p&gt;Granted, we do have to know that &lt;code&gt;initial_guess&lt;&#x2F;code&gt; reads the source text from the database and stores results, which we query using the &lt;code&gt;get_candidates&lt;&#x2F;code&gt; function. However, this doesn&#x27;t violate the aforementioned principle per se, but is a part of the public interface, that is, the contract between the function and its caller. We should aim to keep this interface as narrow as possible and the contracts between modules clear. In the majority of cases, the interface should cover just the function signature, i.e., arguments to the function and what it returns. Nonetheless, in other cases allowing our contract to cover effects in databases and other side-effects doesn&#x27;t add too much to the complexity. In our example, the affected database is provided as a parameter to the function call, so we&#x27;re still being explicit about it.&lt;&#x2F;p&gt;
&lt;p&gt;In our walk up the pyramid, we are moving to higher and higher levels of abstraction. Whereas unit tests could validate internal logic within a module, component integration tests avoid looking into individual modules, test higher-level interfaces, and validate that components work together as intended. When we move to the next level in our hierarchy, we no longer care about the individual components at all, but treat the overall system as a black box.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;api-layer-tests&quot;&gt;API layer tests&lt;a class=&quot;zola-anchor&quot; href=&quot;#api-layer-tests&quot; aria-label=&quot;Anchor link for: api-layer-tests&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The next layer of tests shifts our focus from the internal logic of a service to its external interface. In web services, this usually means testing the HTTP API layer. &lt;a rel=&quot;nofollow noreferrer&quot; href=&quot;https:&#x2F;&#x2F;martinfowler.com&#x2F;bliki&#x2F;SubcutaneousTest.html&quot;&gt;Martin Fowler has called such tests &quot;subcutaneous&quot;&lt;&#x2F;a&gt;, as the API layer sits just beneath the UI surface in a web application.&lt;&#x2F;p&gt;
&lt;p&gt;The API layer tests validate that the service adheres to its contract, i.e., it handles requests appropriately and gives valid responses. In my example project, &lt;a rel=&quot;nofollow noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tonivanhala&#x2F;celery-decipher&#x2F;blob&#x2F;main&#x2F;celery_decipher&#x2F;decipher&#x2F;test_routes.py&quot;&gt;requests and responses are validated using Pydantic models&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span&gt;uuid &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;UUID
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span&gt;celery_decipher.decipher.models &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;DecipherStatusResponse
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;def &lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;test_ingest&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;http_client&lt;&#x2F;span&gt;&lt;span&gt;):
&lt;&#x2F;span&gt;&lt;span&gt;    text = &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;Smoky smoke test&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;    response = http_client.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;post&lt;&#x2F;span&gt;&lt;span&gt;(&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;&#x2F;decipher&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;json&lt;&#x2F;span&gt;&lt;span&gt;={&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;text&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;: text})
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;assert &lt;&#x2F;span&gt;&lt;span&gt;response.status_code == &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;200
&lt;&#x2F;span&gt;&lt;span&gt;    source_text_id = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;UUID&lt;&#x2F;span&gt;&lt;span&gt;(response.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;json&lt;&#x2F;span&gt;&lt;span&gt;()[&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;source_text_id&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;])
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;    status_response = http_client.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;f&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;&#x2F;decipher&#x2F;&lt;&#x2F;span&gt;&lt;span&gt;{source_text_id}&amp;quot;)
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;assert &lt;&#x2F;span&gt;&lt;span&gt;status_response.status_code == &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;200
&lt;&#x2F;span&gt;&lt;span&gt;    status = DecipherStatusResponse.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;model_validate&lt;&#x2F;span&gt;&lt;span&gt;(status_response.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;json&lt;&#x2F;span&gt;&lt;span&gt;())
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;assert &lt;&#x2F;span&gt;&lt;span&gt;status.source_text_id == source_text_id
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;assert &lt;&#x2F;span&gt;&lt;span&gt;status.source_text == text
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;assert &lt;&#x2F;span&gt;&lt;span&gt;status.status == &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;PENDING&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In the example above, the flow of the test is simple. Our service endpoint accepts a text to be deciphered, we receive an unique ID for the request, and we can query the status of the request using that ID. We don&#x27;t know how the service manages these tasks internally and what dependencies, if any, it has for performing the actual deciphering. The API covers the internal logic, providing a shared contract between the service and its clients, while allowing us to treat the service as a black box.&lt;&#x2F;p&gt;
&lt;p&gt;Often the features tested in the API layer require the service to call other services, external dependencies, databases, and so on. Sometimes, you may be able to set up a test environment for all the dependencies, and verify the full flow of the feature. Other times, it&#x27;s not practical for reasons like legacy services without test environments, third-party services with restrictive licensing, complex and time-consuming operations, and so on. In such cases, you can use test doubles for the dependencies, utilizing fakes, stubs and mocks as appropriate. Just be careful! The more you mock, the more complex your test setup becomes, until &lt;strong&gt;you end up verifying the mocks instead of the service&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;end-to-end-tests&quot;&gt;End-to-end tests&lt;a class=&quot;zola-anchor&quot; href=&quot;#end-to-end-tests&quot; aria-label=&quot;Anchor link for: end-to-end-tests&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Finally, we reach the top of the pyramid, where we test complete features and use cases. In web applications, this usually means driving the UI using tools like Selenium, Playwright, or Cypress. In other applications, it may involve prompting the system from upstream services, triggering processing by sending events to message queues, and so on. The difference to API layer tests is the coverage of entire use cases, which often involve multiple services and external systems.&lt;&#x2F;p&gt;
&lt;p&gt;The upstream services may provide a natural way of receiving the results for some use cases, for example, by sending an asynchronous task to an event queue and reading the final result from another queue, or by calling a REST API endpoint and receiving a HTTP response with synchronous results. Other use cases may produce results in an external system, for example, by creating files in a shared storage, sending emails, or updating records in a third-party system. Getting and verifying the results from an external system, and then cleaning them up afterwards, may sometimes be tricky. The required effort is partly why E2E tests can be slow and expensive to maintain.&lt;&#x2F;p&gt;
&lt;p&gt;The example project does not have a frontend or an UI of any kind. The use cases are fully covered by the API layer tests, as the HTTP endpoints implement complete features. The web application drives all the use cases, functioning as the upstream and interface layer. If your application is structured this way, then testing it is straight-forward and simple. However, most real-world systems I&#x27;ve worked with have had multiple entrypoints for different use cases, for example, a web UI for interactive use, REST API endpoints for programmatic access, and batch data processing jobs triggered when data is received from integrations. Covering all these diverse use cases requires significant effort, since we can&#x27;t apply a single template to all of them.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;a class=&quot;zola-anchor&quot; href=&quot;#final-thoughts&quot; aria-label=&quot;Anchor link for: final-thoughts&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;We need tests. They allow me to sleep at night, knowing that my code works as intended. They document how functions behave, how use cases are implemented, and what are the contracts between the components of my system. They allow me to refactor and continuously improve my code, while maintaining quality and mitigating the risk of breaking things.&lt;&#x2F;p&gt;
&lt;p&gt;We need layered tests. Unit tests are great for verifying internal logic of its components, while component integration tests ensure that the components work together. Subcutaneous API layer tests validate the interface contracts, while ensuring appropriate level of abstraction that hides implementation details. Finally, E2E tests cover entire use cases, which ultimately ensure that our software provides value to its users.&lt;&#x2F;p&gt;
&lt;p&gt;We need to learn to love testing. It is a skill that requires practice, identifying patterns and matching them to appropriate test strategies. It should be a natural part of development, whether you&#x27;re doing test-driven development or not.&lt;&#x2F;p&gt;
</content>
	</entry>
	<entry xml:lang="en">
		<title>Google Cloud Storage as S3 - Case Joplin</title>
		<published>2025-03-23T00:00:00+00:00</published>
		<updated>2025-03-23T00:00:00+00:00</updated>
        <summary>&lt;p&gt;I use &lt;a rel=&quot;nofollow noreferrer&quot; href=&quot;https:&#x2F;&#x2F;joplinapp.org&#x2F;&quot;&gt;Joplin&lt;&#x2F;a&gt; as my note-taking app over multiple devices, which makes a shared backend for note-syncing a thing I can&#x27;t live without. Joplin supports a ton of different backends, including AWS S3. Obviously, as a Google enthusiast, I really really wanted to use Google Cloud Storage as my S3 backend. This was a surprisingly easy thing to do.&lt;&#x2F;p&gt;</summary>
		<link href="https://vanhala.org/posts/20250323-cloud-storage-s3/" type="text/html"/>
		<id>https://vanhala.org/posts/20250323-cloud-storage-s3/</id>
		<content type="html">&lt;p&gt;I use &lt;a rel=&quot;nofollow noreferrer&quot; href=&quot;https:&#x2F;&#x2F;joplinapp.org&#x2F;&quot;&gt;Joplin&lt;&#x2F;a&gt; as my note-taking app over multiple devices, which makes a shared backend for note-syncing a thing I can&#x27;t live without. Joplin supports a ton of different backends, including AWS S3. Obviously, as a Google enthusiast, I really really wanted to use Google Cloud Storage as my S3 backend. This was a surprisingly easy thing to do.&lt;&#x2F;p&gt;
&lt;span id=&quot;continue-reading&quot;&gt;&lt;&#x2F;span&gt;
&lt;p&gt;Recently Google Cloud has been my first choice for personal cloud projects. It feels like a very developer-centric platform with developer-centric tooling. Arguably, Google seems to think that &lt;a rel=&quot;nofollow noreferrer&quot; href=&quot;https:&#x2F;&#x2F;www.pluralsight.com&#x2F;resources&#x2F;blog&#x2F;cloud&#x2F;which-google-cloud-certification-is-best-for-me&quot;&gt;everyone working in technology is a developer&lt;&#x2F;a&gt;. This is a sentiment I can get behind, as I aspire to be more of a hacker than an enterprise user.&lt;&#x2F;p&gt;
&lt;p&gt;For this curious case of using Cloud Storage in place of S3, Google has built &lt;a rel=&quot;nofollow noreferrer&quot; href=&quot;https:&#x2F;&#x2F;cloud.google.com&#x2F;storage&#x2F;docs&#x2F;interoperability&quot;&gt;an XML interoperability layer&lt;&#x2F;a&gt;. Getting Joplin to talk to this API was as simple as providing the Cloud Storage URI &lt;code&gt;https:&#x2F;&#x2F;storage.googleapis.com&lt;&#x2F;code&gt; in place of the S3 URL.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;&#x2F;strong&gt; You can find the complete Terraform configuration for this setup in &lt;a rel=&quot;nofollow noreferrer&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;tonivanhala&#x2F;4d4c953abda12ff1522adb30c7d35e9f&quot;&gt;this Gist&lt;&#x2F;a&gt;. Following is a breakdown of the key resources and why you need them.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bucket&quot;&gt;Bucket&lt;a class=&quot;zola-anchor&quot; href=&quot;#bucket&quot; aria-label=&quot;Anchor link for: bucket&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;First, we need a Cloud Storage bucket for the synchronized Joplin state. I like to use a random suffix to ensure the bucket name is unique, as in the following Terraform snippet.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;terraform&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-terraform &quot;&gt;&lt;code class=&quot;language-terraform&quot; data-lang=&quot;terraform&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;resource &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;random_id&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bucket-suffix&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; {
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;byte_length &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;8
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;keepers &lt;&#x2F;span&gt;&lt;span&gt;= {
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;project &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;var&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;project
&lt;&#x2F;span&gt;&lt;span&gt;  }
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;resource &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;google_storage_bucket&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;sync&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; {
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;name                     &lt;&#x2F;span&gt;&lt;span&gt;= &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;joplin-&lt;&#x2F;span&gt;&lt;span&gt;${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;random_id&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;bucket-suffix&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;hex&lt;&#x2F;span&gt;&lt;span&gt;}&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;public_access_prevention &lt;&#x2F;span&gt;&lt;span&gt;= &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;enforced&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;location                 &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;var&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;region
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;project                  &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;var&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;project
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Note that I am using Terraform variables &lt;code&gt;project&lt;&#x2F;code&gt; and &lt;code&gt;region&lt;&#x2F;code&gt; throughout these examples.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;service-account&quot;&gt;Service Account&lt;a class=&quot;zola-anchor&quot; href=&quot;#service-account&quot; aria-label=&quot;Anchor link for: service-account&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Then, I recommend creating a separate service account with minimum permissions to access the bucket. Good infosec hygiene keeps the setup less smelly.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;terraform&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-terraform &quot;&gt;&lt;code class=&quot;language-terraform&quot; data-lang=&quot;terraform&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;resource &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;google_service_account&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;sync&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; {
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;account_id   &lt;&#x2F;span&gt;&lt;span&gt;= &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;sync-bucket&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;display_name &lt;&#x2F;span&gt;&lt;span&gt;= &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;Joplin sync bucket service account&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;resource &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;google_storage_bucket_iam_member&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;sync&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; {
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;bucket &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;google_storage_bucket&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;sync&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;name
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;role   &lt;&#x2F;span&gt;&lt;span&gt;= &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;roles&#x2F;storage.objectUser&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;member &lt;&#x2F;span&gt;&lt;span&gt;= &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;serviceAccount:&lt;&#x2F;span&gt;&lt;span&gt;${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;google_service_account&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;sync&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;email&lt;&#x2F;span&gt;&lt;span&gt;}&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;roles&#x2F;storage.objectUser&lt;&#x2F;code&gt; gives the account &lt;a rel=&quot;nofollow noreferrer&quot; href=&quot;https:&#x2F;&#x2F;cloud.google.com&#x2F;storage&#x2F;docs&#x2F;access-control&#x2F;iam-roles&quot;&gt;permission to do what it likes with objects and folders&lt;&#x2F;a&gt; in the bucket, without allowing it to modify access and permissions. Joplin likes to create and delete both objects and folders, so this is the best fit from predefined roles for GCS.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;access-key-and-secret&quot;&gt;Access key and secret&lt;a class=&quot;zola-anchor&quot; href=&quot;#access-key-and-secret&quot; aria-label=&quot;Anchor link for: access-key-and-secret&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Access to S3 is authenticated with an access key and secret. We can generate HMAC keys for the service account and push them to Secrets Manager.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;terraform&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-terraform &quot;&gt;&lt;code class=&quot;language-terraform&quot; data-lang=&quot;terraform&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;resource &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;google_storage_hmac_key&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;sync&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; {
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;service_account_email &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;google_service_account&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;sync&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;email
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;resource &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;google_secret_manager_secret&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;hmac-secret&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; {
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;secret_id &lt;&#x2F;span&gt;&lt;span&gt;= &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;hmac-secret&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;  replication {
&lt;&#x2F;span&gt;&lt;span&gt;    auto {}
&lt;&#x2F;span&gt;&lt;span&gt;  }
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;resource &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;google_secret_manager_secret&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;hmac-id&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; {
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;secret_id &lt;&#x2F;span&gt;&lt;span&gt;= &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;hmac-id&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;  replication {
&lt;&#x2F;span&gt;&lt;span&gt;    auto {}
&lt;&#x2F;span&gt;&lt;span&gt;  }
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;resource &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;google_secret_manager_secret_version&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;hmac-secret&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; {
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;secret      &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;google_secret_manager_secret&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;hmac-secret&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;id
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;secret_data &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;google_storage_hmac_key&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;sync&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;secret
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;resource &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;google_secret_manager_secret_version&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;hmac-id&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; {
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;secret      &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;google_secret_manager_secret&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;hmac-id&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;id
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;secret_data &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;google_storage_hmac_key&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;sync&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;access_id
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;joplin-configuration&quot;&gt;Joplin configuration&lt;a class=&quot;zola-anchor&quot; href=&quot;#joplin-configuration&quot; aria-label=&quot;Anchor link for: joplin-configuration&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Finally, we can &lt;a rel=&quot;nofollow noreferrer&quot; href=&quot;https:&#x2F;&#x2F;joplinapp.org&#x2F;help&#x2F;apps&#x2F;sync&#x2F;s3&quot;&gt;configure Joplin&lt;&#x2F;a&gt; to use the bucket and HMAC key. The Google parameters match S3 settings pretty closely.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;S3 bucket&lt;&#x2F;strong&gt;: output of &lt;code&gt;google_storage_bucket.sync.name&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;S3 URL&lt;&#x2F;strong&gt;: &lt;code&gt;storage.googleapis.com&lt;&#x2F;code&gt; (Google&#x27;s XML API).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;S3 region&lt;&#x2F;strong&gt;: your &lt;code&gt;var.region&lt;&#x2F;code&gt; (Google Cloud region).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;The S3 access key&lt;&#x2F;strong&gt;: HMAC ID (fetch from Secrets Manager, e.g., &lt;code&gt;gcloud secrets versions access latest --secret hmac-id&lt;&#x2F;code&gt;).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;The S3 access secret&lt;&#x2F;strong&gt;: HMAC secret (fetch from Secrets Manager).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
	</entry>
</feed>