@@ -23,9 +23,10 @@ var schemaDDL string
2323
2424// TestContainer holds the test database container, connection pool, and repository instances.
2525type TestContainer struct {
26- container * postgres.PostgresContainer
27- Pool * pgxpool.Pool
28- Repos * persistence.Repositories
26+ container * postgres.PostgresContainer
27+ Pool * pgxpool.Pool
28+ Repos * persistence.Repositories
29+ MasterTenantID tenant.TenantID // The master tenant used for shared/hierarchical data lookups
2930}
3031
3132// SetupTestContainer creates a PostgreSQL testcontainer with the market_information schema loaded.
@@ -59,16 +60,42 @@ func SetupTestContainer(t *testing.T) *TestContainer {
5960 pool , err := pgxpool .NewWithConfig (ctx , poolConfig )
6061 require .NoError (t , err , "Failed to create connection pool" )
6162
62- // Load schema
63+ // Load schema into public schema (used by non-tenant-scoped tests)
6364 loadSchema (t , pool )
6465
66+ // Create master tenant schema with the same tables.
67+ // The market-information service uses a master tenant for hierarchical lookup
68+ // (shared dataset fallback). Without public in search_path, the master tenant
69+ // needs its own schema with all required tables.
70+ masterTenantID := "test_master"
71+ masterSchema := "org_" + masterTenantID
72+ _ , err = pool .Exec (ctx , "CREATE SCHEMA IF NOT EXISTS " + masterSchema )
73+ require .NoError (t , err , "Failed to create master tenant schema" )
74+ err = loadSchemaInSchema (ctx , pool , masterSchema )
75+ require .NoError (t , err , "Failed to load schema into master tenant schema" )
76+
77+ // Set default search_path to master tenant schema for non-tenant-scoped queries.
78+ // Tests that don't explicitly set a tenant context will use the master schema,
79+ // matching production behavior where the master tenant is the default context.
80+ _ , err = pool .Exec (ctx , "ALTER DATABASE test_market_information SET search_path TO " + masterSchema )
81+ require .NoError (t , err , "Failed to set default search_path" )
82+
83+ // Reconnect to pick up the new default search_path
84+ pool .Close ()
85+ pool , err = pgxpool .NewWithConfig (ctx , poolConfig )
86+ require .NoError (t , err , "Failed to reconnect with new search_path" )
87+
6588 // Create repositories with test master tenant
66- repos := persistence .NewRepositories (pool , "test_master" )
89+ repos := persistence .NewRepositories (pool , masterTenantID )
90+
91+ masterTID , err := tenant .NewTenantID (masterTenantID )
92+ require .NoError (t , err , "Failed to parse master tenant ID" )
6793
6894 return & TestContainer {
69- container : pgContainer ,
70- Pool : pool ,
71- Repos : repos ,
95+ container : pgContainer ,
96+ Pool : pool ,
97+ Repos : repos ,
98+ MasterTenantID : masterTID ,
7299 }
73100}
74101
@@ -125,30 +152,31 @@ func (tc *TestContainer) CreateTenantSchema(tenantIDStr string) (tenant.TenantID
125152 return tenant .TenantID ("" ), fmt .Errorf ("failed to load schema: %w" , err )
126153 }
127154
128- // Copy shared datasets from public schema to tenant schema.
129- // This allows tenant observations to reference shared dataset definitions.
130- // Note: quotedSchema is safely quoted via pgx.Identifier.Sanitize() above.
155+ // Copy shared datasets from master schema to tenant schema.
156+ // Reference data is saved to the master schema (org_test_master) via the default search_path,
157+ // not to public. This allows tenant observations to reference shared dataset definitions.
158+ masterSchema := pgx.Identifier {tc .MasterTenantID .SchemaName ()}.Sanitize ()
131159 _ , err = tc .Pool .Exec (ctx , fmt .Sprintf (`
132160 INSERT INTO %s.dataset_definition
133- SELECT * FROM public .dataset_definition
161+ SELECT * FROM %s .dataset_definition
134162 WHERE is_shared = TRUE
135- ` , quotedSchema ))
163+ ` , quotedSchema , masterSchema ))
136164 if err != nil {
137165 return tenant .TenantID ("" ), fmt .Errorf ("failed to copy shared datasets: %w" , err )
138166 }
139167
140- // Copy data sources from public schema to tenant schema
168+ // Copy data sources from master schema to tenant schema
141169 // Data sources are needed for observations to have valid foreign keys
142170 _ , err = tc .Pool .Exec (ctx , fmt .Sprintf (`
143171 INSERT INTO %s.data_source
144- SELECT * FROM public .data_source
145- ` , quotedSchema ))
172+ SELECT * FROM %s .data_source
173+ ` , quotedSchema , masterSchema ))
146174 if err != nil {
147175 return tenant .TenantID ("" ), fmt .Errorf ("failed to copy data sources: %w" , err )
148176 }
149177
150- // Reset search path to default
151- _ , err = tc .Pool .Exec (ctx , "SET search_path TO public" )
178+ // Reset search path to master schema (the database default)
179+ _ , err = tc .Pool .Exec (ctx , "SET search_path TO " + masterSchema )
152180 if err != nil {
153181 return tenant .TenantID ("" ), fmt .Errorf ("failed to reset search_path: %w" , err )
154182 }
@@ -161,10 +189,19 @@ func (tc *TestContainer) WithTenant(ctx context.Context, tenantID tenant.TenantI
161189 return tenant .WithTenant (ctx , tenantID )
162190}
163191
192+ // MasterContext returns a context scoped to the master tenant.
193+ // Use this when saving reference data (datasets, data sources) that the observation
194+ // repository's hierarchical lookup will query via the master tenant schema.
195+ func (tc * TestContainer ) MasterContext (ctx context.Context ) context.Context {
196+ return tenant .WithTenant (ctx , tc .MasterTenantID )
197+ }
198+
164199// GrantTenantEntitlement grants access to a dataset for a tenant.
200+ // Uses public.tenant_data_entitlements explicitly because the production code's
201+ // checkTenantAccess queries public.tenant_data_entitlements directly.
165202func (tc * TestContainer ) GrantTenantEntitlement (ctx context.Context , tenantID tenant.TenantID , datasetCode string , expiresAt * time.Time ) error {
166203 query := `
167- INSERT INTO tenant_data_entitlements (tenant_id, dataset_code, is_active, expires_at)
204+ INSERT INTO public. tenant_data_entitlements (tenant_id, dataset_code, is_active, expires_at)
168205 VALUES ($1, $2, TRUE, $3)
169206 ON CONFLICT (tenant_id, dataset_code)
170207 DO UPDATE SET is_active = TRUE, expires_at = EXCLUDED.expires_at`
@@ -177,9 +214,10 @@ func (tc *TestContainer) GrantTenantEntitlement(ctx context.Context, tenantID te
177214}
178215
179216// RevokeTenantEntitlement revokes access to a dataset for a tenant.
217+ // Uses public.tenant_data_entitlements explicitly to match checkTenantAccess.
180218func (tc * TestContainer ) RevokeTenantEntitlement (ctx context.Context , tenantID tenant.TenantID , datasetCode string ) error {
181219 query := `
182- UPDATE tenant_data_entitlements
220+ UPDATE public. tenant_data_entitlements
183221 SET is_active = FALSE
184222 WHERE tenant_id = $1 AND dataset_code = $2`
185223
0 commit comments