Skip to content

Commit 83cdb90

Browse files
committed
tests: WIP, not all running
1 parent bb06d9b commit 83cdb90

File tree

11 files changed

+1050
-483
lines changed

11 files changed

+1050
-483
lines changed

src/lambdas/api/app.js

Lines changed: 449 additions & 446 deletions
Large diffs are not rendered by default.

src/lambdas/api/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import { z } from 'zod'
1010
import serverless from 'serverless-http'
1111
import { Lambda } from '@aws-sdk/client-lambda'
12-
import { app } from './app.js'
12+
import { createApp } from './app.js'
1313
import _default from './types.js'
1414
import logger from '../../lib/logger.js'
1515

@@ -156,13 +156,19 @@ const invokePostHook = async (lambda, postHook, payload) => {
156156
return hookResult
157157
}
158158

159+
let appInstance = null
160+
159161
/**
160162
* @param {APIGatewayProxyEvent} event
161163
* @param {Context} context
162164
* @returns {Promise<APIGatewayProxyResult>}
163165
*/
164166
const callServerlessApp = async (event, context) => {
165-
const result = await serverless(app)(event, context)
167+
if (!appInstance) {
168+
appInstance = await createApp()
169+
}
170+
171+
const result = await serverless(appInstance)(event, context)
166172

167173
try {
168174
return APIGatewayProxyResultSchema.parse(result)

src/lambdas/api/local.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import winston from 'winston'
2-
import { app } from './app.js'
2+
import { createApp } from './app.js'
33

44
const logger = winston.createLogger({
55
level: process.env['LOG_LEVEL'] || 'warn',
@@ -11,6 +11,7 @@ const logger = winston.createLogger({
1111

1212
const port = 3000
1313

14+
const app = await createApp()
1415
app.listen(port, () => {
1516
logger.warn(`stac-server listening on port ${port}`)
1617
})

src/lib/asset-proxy.js

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ export const BucketOption = Object.freeze({
99
LIST: 'LIST'
1010
})
1111

12-
// Cached configuration - initialized once at startup
13-
let cachedProxyConfig = null
12+
// Cached configuration
13+
let proxyConfigCache = null
1414

1515
// Cached S3 clients by region to avoid creating new clients on each request
1616
const s3ClientCache = new Map()
@@ -55,76 +55,87 @@ const fetchAllBucketsInAccount = async () => {
5555
}
5656

5757
/**
58-
* Load and cache proxy configuration from environment variables
59-
* This function is called once at app startup and the result is cached.
58+
* Initialize asset proxy configuration.
59+
* The config is cached after first initialization. Subsequent calls return the cached value.
6060
* @returns {Promise<Object>} Configuration object
6161
*/
62-
export const getProxyConfig = async () => {
63-
// Return cached config if already loaded
64-
if (cachedProxyConfig) {
65-
return cachedProxyConfig
62+
export const initProxyConfig = async () => {
63+
if (proxyConfigCache) {
64+
return proxyConfigCache
6665
}
6766

6867
const bucketOption = process.env['ASSET_PROXY_BUCKET_OPTION'] || BucketOption.NONE
6968
const bucketList = process.env['ASSET_PROXY_BUCKET_LIST'] || ''
7069
const urlExpiry = parseInt(process.env['ASSET_PROXY_URL_EXPIRY'] || '300', 10)
7170

72-
if (bucketOption === BucketOption.NONE) {
73-
cachedProxyConfig = {
71+
switch (bucketOption) {
72+
case BucketOption.NONE:
73+
proxyConfigCache = {
7474
enabled: false,
7575
mode: BucketOption.NONE,
7676
buckets: new Set(),
7777
urlExpiry
7878
}
79-
} else if (bucketOption === BucketOption.ALL) {
80-
cachedProxyConfig = {
79+
break
80+
81+
case BucketOption.ALL:
82+
proxyConfigCache = {
8183
enabled: true,
8284
mode: BucketOption.ALL,
8385
buckets: new Set(),
8486
urlExpiry
8587
}
86-
} else if (bucketOption === BucketOption.ALL_BUCKETS_IN_ACCOUNT) {
88+
break
89+
90+
case BucketOption.ALL_BUCKETS_IN_ACCOUNT: {
8791
const buckets = await fetchAllBucketsInAccount()
88-
cachedProxyConfig = {
92+
proxyConfigCache = {
8993
enabled: true,
9094
mode: BucketOption.ALL_BUCKETS_IN_ACCOUNT,
9195
buckets,
9296
urlExpiry
9397
}
94-
} else if (bucketOption === BucketOption.LIST) {
98+
break
99+
}
100+
101+
case BucketOption.LIST: {
95102
const buckets = bucketList.split(',').map((b) => b.trim()).filter((b) => b)
96-
cachedProxyConfig = {
103+
proxyConfigCache = {
97104
enabled: true,
98105
mode: BucketOption.LIST,
99106
buckets: new Set(buckets),
100107
urlExpiry
101108
}
102-
} else {
109+
break
110+
}
111+
112+
default: {
103113
const validOptions = Object.values(BucketOption).join(', ')
104114
throw new Error(
105115
`Invalid ASSET_PROXY_BUCKET_OPTION: ${bucketOption}. Must be one of: ${validOptions}`
106116
)
107117
}
118+
}
108119

109120
logger.debug('Asset proxy configuration loaded', {
110-
mode: cachedProxyConfig.mode,
111-
enabled: cachedProxyConfig.enabled,
112-
bucketCount: cachedProxyConfig.buckets.size,
113-
urlExpiry: cachedProxyConfig.urlExpiry
121+
mode: proxyConfigCache.mode,
122+
enabled: proxyConfigCache.enabled,
123+
bucketCount: proxyConfigCache.buckets.size,
124+
urlExpiry: proxyConfigCache.urlExpiry
114125
})
115126

116-
return cachedProxyConfig
127+
return proxyConfigCache
117128
}
118129

119130
/**
120131
* Get the cached proxy configuration synchronously
121132
* @returns {Object} Cached configuration object
122133
*/
123134
export const getCachedProxyConfig = () => {
124-
if (!cachedProxyConfig) {
125-
throw new Error('Asset proxy config not initialized. Call getProxyConfig() at startup.')
135+
if (!proxyConfigCache) {
136+
throw new Error('Asset proxy config not initialized.')
126137
}
127-
return cachedProxyConfig
138+
return proxyConfigCache
128139
}
129140

130141
/**
@@ -140,10 +151,6 @@ export const getCachedProxyConfig = () => {
140151
* @returns {Object|null} {bucket, key, region} or null if not a valid S3 URL
141152
*/
142153
export const parseS3Url = (url) => {
143-
if (!url || typeof url !== 'string') {
144-
return null
145-
}
146-
147154
// S3 URI format: s3://bucket/key
148155
if (url.startsWith('s3://')) {
149156
const withoutProtocol = url.substring(5)
@@ -184,8 +191,9 @@ export const parseS3Url = (url) => {
184191
return { bucket, key, region }
185192
}
186193

187-
// Path style: s3.region.amazonaws.com/bucket/key or s3.amazonaws.com/bucket/key
188-
const pathStyleMatch = hostname.match(/^s3(?:\.([^.]+))?\.amazonaws\.com$/)
194+
// Path style: s3.region.amazonaws.com/bucket/key,
195+
// s3-region.amazonaws.com/bucket/key, or s3.amazonaws.com/bucket/key
196+
const pathStyleMatch = hostname.match(/^s3(?:[.-]([^.]+))?\.amazonaws\.com$/)
189197
if (pathStyleMatch) {
190198
const region = pathStyleMatch[1] || null
191199
const pathParts = pathname.split('/').filter((p) => p)
@@ -334,7 +342,7 @@ export const determineS3Region = (asset, itemOrCollection) => {
334342
}
335343

336344
export default {
337-
getProxyConfig,
345+
initProxyConfig,
338346
getCachedProxyConfig,
339347
parseS3Url,
340348
shouldProxyAssets,

src/lib/database-client.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export async function dbClient() {
7474
return _dbClient
7575
}
7676

77-
export async function createIndex(index) {
77+
export async function createIndecreateIndexcreateIndexx(index) {
7878
const client = await dbClient()
7979
const exists = await client.indices.exists({ index })
8080
if (!exists.body) {

tests/helpers/api.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import got from 'got' // eslint-disable-line import/no-unresolved
22
import { once } from 'events'
3-
import { app } from '../../src/lambdas/api/app.js'
3+
import { createApp } from '../../src/lambdas/api/app.js'
44

55
/**
66
* @typedef {import('got').Got} Got
@@ -30,6 +30,7 @@ const apiClient = (url) => got.extend({
3030
* @returns {Promise<ApiInstance>}
3131
*/
3232
export const startApi = async () => {
33+
const app = await createApp()
3334
const server = app.listen(0, '127.0.0.1')
3435

3536
await once(server, 'listening')
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// @ts-nocheck
2+
3+
/**
4+
* Asset Proxy System Tests
5+
*
6+
* These tests verify the asset proxy endpoints work correctly.
7+
* The env var is set before starting the API to test with proxying enabled.
8+
*/
9+
10+
// Set env var before starting the API
11+
process.env['ASSET_PROXY_BUCKET_OPTION'] = 'ALL'
12+
13+
/* eslint-disable import/first */
14+
import test from 'ava'
15+
import { deleteAllIndices } from '../helpers/database.js'
16+
import { ingestItem } from '../helpers/ingest.js'
17+
import { randomId, loadFixture } from '../helpers/utils.js'
18+
import { setup } from '../helpers/system-tests.js'
19+
/* eslint-enable import/first */
20+
21+
test.before(async (t) => {
22+
await deleteAllIndices()
23+
const standUpResult = await setup()
24+
25+
t.context = standUpResult
26+
27+
t.context.collectionId = randomId('collection')
28+
29+
const collection = await loadFixture(
30+
'landsat-8-l1-collection.json',
31+
{ id: t.context.collectionId }
32+
)
33+
34+
await ingestItem({
35+
ingestQueueUrl: t.context.ingestQueueUrl,
36+
ingestTopicArn: t.context.ingestTopicArn,
37+
item: collection
38+
})
39+
40+
t.context.itemId = randomId('item')
41+
42+
const item = await loadFixture(
43+
'stac/LC80100102015082LGN00.json',
44+
{
45+
id: t.context.itemId,
46+
collection: t.context.collectionId
47+
}
48+
)
49+
50+
await ingestItem({
51+
ingestQueueUrl: t.context.ingestQueueUrl,
52+
ingestTopicArn: t.context.ingestTopicArn,
53+
item
54+
})
55+
})
56+
57+
test.after.always(async (t) => {
58+
if (t.context.api) await t.context.api.close()
59+
})
60+
61+
test('GET /collections/:collectionId/items/:itemId/assets/:assetKey - 302 redirect to presigned URL', async (t) => {
62+
const { collectionId, itemId } = t.context
63+
64+
const response = await t.context.api.client.get(
65+
`collections/${collectionId}/items/${itemId}/assets/B1`,
66+
{
67+
resolveBodyOnly: false,
68+
throwHttpErrors: false,
69+
followRedirect: false
70+
}
71+
)
72+
73+
t.is(response.statusCode, 302)
74+
t.truthy(response.headers.location)
75+
t.true(response.headers.location.includes('landsat-pds'))
76+
t.true(response.headers.location.includes('X-Amz-Algorithm'))
77+
t.true(response.headers.location.includes('X-Amz-Signature'))
78+
})
79+
80+
test('GET /collections/:collectionId/assets/:assetKey - 302 redirect for collection assets', async (t) => {
81+
const { collectionId } = t.context
82+
83+
const collection = await t.context.api.client.get(
84+
`collections/${collectionId}`,
85+
{ resolveBodyOnly: false }
86+
)
87+
88+
if (!collection.body.assets || Object.keys(collection.body.assets).length === 0) {
89+
t.pass('Collection has no assets to test')
90+
return
91+
}
92+
93+
const assetKey = Object.keys(collection.body.assets)[0]
94+
95+
const response = await t.context.api.client.get(
96+
`collections/${collectionId}/assets/${assetKey}`,
97+
{
98+
resolveBodyOnly: false,
99+
throwHttpErrors: false,
100+
followRedirect: false
101+
}
102+
)
103+
104+
t.is(response.statusCode, 302)
105+
t.truthy(response.headers.location)
106+
t.true(response.headers.location.includes('X-Amz-Algorithm'))
107+
})
108+
109+
test('GET /collections/:collectionId/items/:itemId/assets/:assetKey - 404 for non-existent asset', async (t) => {
110+
const { collectionId, itemId } = t.context
111+
112+
const response = await t.context.api.client.get(
113+
`collections/${collectionId}/items/${itemId}/assets/DOES_NOT_EXIST`,
114+
{
115+
resolveBodyOnly: false,
116+
throwHttpErrors: false
117+
}
118+
)
119+
120+
t.is(response.statusCode, 404)
121+
})
122+
123+
test('GET /collections/:collectionId/items/:itemId/assets/:assetKey - 404 for non-existent item', async (t) => {
124+
const { collectionId } = t.context
125+
126+
const response = await t.context.api.client.get(
127+
`collections/${collectionId}/items/DOES_NOT_EXIST/assets/B1`,
128+
{
129+
resolveBodyOnly: false,
130+
throwHttpErrors: false
131+
}
132+
)
133+
134+
t.is(response.statusCode, 404)
135+
})
136+
137+
test('GET /collections/:collectionId/items/:itemId/assets/:assetKey - 404 for non-existent collection', async (t) => {
138+
const response = await t.context.api.client.get(
139+
'collections/DOES_NOT_EXIST/items/DOES_NOT_EXIST/assets/B1',
140+
{
141+
resolveBodyOnly: false,
142+
throwHttpErrors: false
143+
}
144+
)
145+
146+
t.is(response.statusCode, 404)
147+
})

0 commit comments

Comments
 (0)