@sitblueprint/website-construct 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/docs/CHANGELOG.md +43 -0
- package/lib/index.d.ts +28 -0
- package/lib/index.js +109 -0
- package/lib/index.ts +137 -0
- package/package.json +2 -2
- package/test/website-construct.test.d.ts +1 -0
- package/test/website-construct.test.js +283 -0
- package/test/website-construct.test.ts +321 -0
- package/.github/workflows/prettier.yml +0 -18
- package/.github/workflows/test.yaml +0 -24
package/README.md
CHANGED
|
@@ -9,8 +9,8 @@ A reusable [AWS CDK](https://docs.aws.amazon.com/cdk/) construct to deploy a web
|
|
|
9
9
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
|
12
|
-
```
|
|
13
|
-
npm
|
|
12
|
+
```bash
|
|
13
|
+
npm i @sitblueprint/website-construct
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
## Usage
|
|
@@ -20,7 +20,7 @@ export class MyWebsiteStack extends cdk.Stack {
|
|
|
20
20
|
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
|
|
21
21
|
super(scope, id, props);
|
|
22
22
|
|
|
23
|
-
new
|
|
23
|
+
new Website(this, "MyWebsite", {
|
|
24
24
|
bucketName: "my-static-site-bucket",
|
|
25
25
|
indexFile: "index.html",
|
|
26
26
|
errorFile: "error.html",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [v0.1.1] - 2025-09-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `DomainConfig` for custom domains (domain + subdomain + ACM certificate).
|
|
15
|
+
- Route 53 `ARecord` creation for CloudFront distribution.
|
|
16
|
+
- Support for custom 404 pages (`notFoundResponsePagePath`).
|
|
17
|
+
- CloudFormation outputs:
|
|
18
|
+
- CloudFront Distribution URL
|
|
19
|
+
- S3 Website URL
|
|
20
|
+
- Custom domain URL (if configured)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- CloudFront distribution defaults:
|
|
25
|
+
- Redirect all traffic to HTTPS.
|
|
26
|
+
- Cache 404 responses for 30 minutes.
|
|
27
|
+
- Price class limited to `PRICE_CLASS_100` for lower cost.
|
|
28
|
+
|
|
29
|
+
### Security
|
|
30
|
+
|
|
31
|
+
- Enforced access restrictions with CloudFront Origin Access Identity (OAI).
|
|
32
|
+
- Buckets encrypted with **S3 Managed Encryption** by default.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## [v0.1.0] - 2025-08-15
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- Initial release of Website.
|
|
41
|
+
- Static website hosting via S3.
|
|
42
|
+
- CloudFront distribution with default caching.
|
|
43
|
+
- Basic index and error page support.
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
export interface DomainConfig {
|
|
3
|
+
/** The root domain name (e.g., example.com).
|
|
4
|
+
* There must be an associated hosted zone in Route 53 for this domain.
|
|
5
|
+
*/
|
|
6
|
+
domainName: string;
|
|
7
|
+
/** The subdomain name */
|
|
8
|
+
subdomainName: string;
|
|
9
|
+
/** The ARN of the SSL certificate to use for the domain. */
|
|
10
|
+
certificateArn: string;
|
|
11
|
+
}
|
|
12
|
+
export interface WebsiteProps {
|
|
13
|
+
/** The name of the S3 bucket that will host the website content. */
|
|
14
|
+
bucketName: string;
|
|
15
|
+
/** The path to the index document that will be served as the default page. */
|
|
16
|
+
indexFile: string;
|
|
17
|
+
/** The path to the error document that will be served when an error occurs. */
|
|
18
|
+
errorFile: string;
|
|
19
|
+
/** Optional configuration for custom domain setup. */
|
|
20
|
+
domainConfig?: DomainConfig;
|
|
21
|
+
/** Optional path to a custom 404 page. If not specified, the error file will be used. */
|
|
22
|
+
notFoundResponsePagePath?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class Website extends Construct {
|
|
25
|
+
constructor(scope: Construct, id: string, props: WebsiteProps);
|
|
26
|
+
private _getFullDomainName;
|
|
27
|
+
private _getCertificate;
|
|
28
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.Website = void 0;
|
|
27
|
+
const constructs_1 = require("constructs");
|
|
28
|
+
const s3 = __importStar(require("aws-cdk-lib/aws-s3"));
|
|
29
|
+
const cloudfont = __importStar(require("aws-cdk-lib/aws-cloudfront"));
|
|
30
|
+
const origins = __importStar(require("aws-cdk-lib/aws-cloudfront-origins"));
|
|
31
|
+
const certificatemanager = __importStar(require("aws-cdk-lib/aws-certificatemanager"));
|
|
32
|
+
const cdk = __importStar(require("aws-cdk-lib"));
|
|
33
|
+
const iam = __importStar(require("aws-cdk-lib/aws-iam"));
|
|
34
|
+
const route53 = __importStar(require("aws-cdk-lib/aws-route53"));
|
|
35
|
+
class Website extends constructs_1.Construct {
|
|
36
|
+
constructor(scope, id, props) {
|
|
37
|
+
super(scope, id);
|
|
38
|
+
const webBucket = new s3.Bucket(this, props.bucketName, {
|
|
39
|
+
websiteIndexDocument: props.indexFile,
|
|
40
|
+
websiteErrorDocument: props.errorFile,
|
|
41
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
42
|
+
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
43
|
+
});
|
|
44
|
+
const oai = new cloudfont.OriginAccessIdentity(this, `${props.bucketName}-OAI`);
|
|
45
|
+
webBucket.addToResourcePolicy(new iam.PolicyStatement({
|
|
46
|
+
actions: ["s3:GetObject"],
|
|
47
|
+
resources: [webBucket.arnForObjects("*")],
|
|
48
|
+
principals: [
|
|
49
|
+
new iam.CanonicalUserPrincipal(oai.cloudFrontOriginAccessIdentityS3CanonicalUserId),
|
|
50
|
+
],
|
|
51
|
+
}));
|
|
52
|
+
const distribution = new cloudfont.Distribution(this, `${props.bucketName}-distribution`, {
|
|
53
|
+
defaultBehavior: {
|
|
54
|
+
origin: new origins.S3StaticWebsiteOrigin(webBucket),
|
|
55
|
+
viewerProtocolPolicy: cloudfont.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
56
|
+
},
|
|
57
|
+
errorResponses: [
|
|
58
|
+
{
|
|
59
|
+
httpStatus: 404,
|
|
60
|
+
responseHttpStatus: 404,
|
|
61
|
+
responsePagePath: props.notFoundResponsePagePath || `/404.html`,
|
|
62
|
+
ttl: cdk.Duration.minutes(30),
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
priceClass: cloudfont.PriceClass.PRICE_CLASS_100,
|
|
66
|
+
...(props.domainConfig
|
|
67
|
+
? {
|
|
68
|
+
domainNames: [this._getFullDomainName(props.domainConfig)],
|
|
69
|
+
certificate: this._getCertificate(props.domainConfig.certificateArn),
|
|
70
|
+
}
|
|
71
|
+
: {}),
|
|
72
|
+
});
|
|
73
|
+
if (props.domainConfig) {
|
|
74
|
+
const hostedZone = route53.HostedZone.fromLookup(this, "HostedZone", {
|
|
75
|
+
domainName: props.domainConfig.domainName,
|
|
76
|
+
});
|
|
77
|
+
const domainARecord = new route53.ARecord(this, "DomainARecord", {
|
|
78
|
+
zone: hostedZone,
|
|
79
|
+
recordName: this._getFullDomainName(props.domainConfig),
|
|
80
|
+
target: cdk.aws_route53.RecordTarget.fromAlias(new cdk.aws_route53_targets.CloudFrontTarget(distribution)),
|
|
81
|
+
});
|
|
82
|
+
domainARecord.node.addDependency(distribution);
|
|
83
|
+
}
|
|
84
|
+
new cdk.CfnOutput(this, "cloudfront-website-url", {
|
|
85
|
+
value: distribution.distributionDomainName,
|
|
86
|
+
description: "CloudFront Distribution Domain Name",
|
|
87
|
+
});
|
|
88
|
+
new cdk.CfnOutput(this, "s3-website-url", {
|
|
89
|
+
value: webBucket.bucketWebsiteUrl,
|
|
90
|
+
description: "S3 Bucket Website URL",
|
|
91
|
+
});
|
|
92
|
+
if (props.domainConfig) {
|
|
93
|
+
new cdk.CfnOutput(this, "website-url", {
|
|
94
|
+
value: distribution.domainName,
|
|
95
|
+
description: "Website URL",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
_getFullDomainName(domainConfig) {
|
|
100
|
+
return domainConfig.subdomainName
|
|
101
|
+
? `${domainConfig.subdomainName}.${domainConfig.domainName}`
|
|
102
|
+
: domainConfig.domainName;
|
|
103
|
+
}
|
|
104
|
+
_getCertificate(arn) {
|
|
105
|
+
return certificatemanager.Certificate.fromCertificateArn(this, `website-cert`, arn);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.Website = Website;
|
|
109
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQUFBLDJDQUF1QztBQUN2Qyx1REFBeUM7QUFDekMsc0VBQXdEO0FBQ3hELDRFQUE4RDtBQUM5RCx1RkFBeUU7QUFDekUsaURBQW1DO0FBQ25DLHlEQUEyQztBQUMzQyxpRUFBbUQ7QUE4Qm5ELE1BQWEsT0FBUSxTQUFRLHNCQUFTO0lBQ3BDLFlBQVksS0FBZ0IsRUFBRSxFQUFVLEVBQUUsS0FBbUI7UUFDM0QsS0FBSyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsQ0FBQztRQUNqQixNQUFNLFNBQVMsR0FBRyxJQUFJLEVBQUUsQ0FBQyxNQUFNLENBQUMsSUFBSSxFQUFFLEtBQUssQ0FBQyxVQUFVLEVBQUU7WUFDdEQsb0JBQW9CLEVBQUUsS0FBSyxDQUFDLFNBQVM7WUFDckMsb0JBQW9CLEVBQUUsS0FBSyxDQUFDLFNBQVM7WUFDckMsYUFBYSxFQUFFLEdBQUcsQ0FBQyxhQUFhLENBQUMsT0FBTztZQUN4QyxVQUFVLEVBQUUsRUFBRSxDQUFDLGdCQUFnQixDQUFDLFVBQVU7U0FDM0MsQ0FBQyxDQUFDO1FBQ0gsTUFBTSxHQUFHLEdBQUcsSUFBSSxTQUFTLENBQUMsb0JBQW9CLENBQzVDLElBQUksRUFDSixHQUFHLEtBQUssQ0FBQyxVQUFVLE1BQU0sQ0FDMUIsQ0FBQztRQUNGLFNBQVMsQ0FBQyxtQkFBbUIsQ0FDM0IsSUFBSSxHQUFHLENBQUMsZUFBZSxDQUFDO1lBQ3RCLE9BQU8sRUFBRSxDQUFDLGNBQWMsQ0FBQztZQUN6QixTQUFTLEVBQUUsQ0FBQyxTQUFTLENBQUMsYUFBYSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQ3pDLFVBQVUsRUFBRTtnQkFDVixJQUFJLEdBQUcsQ0FBQyxzQkFBc0IsQ0FDNUIsR0FBRyxDQUFDLCtDQUErQyxDQUNwRDthQUNGO1NBQ0YsQ0FBQyxDQUNILENB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QUFrQixDQUN0RCxJQUFJLEVBQ0osY0FBYyxFQUNkLEdBQUcsQ0FDSixDQUFDO0lBQ0osQ0FBQztDQUNGO0FBbkdELDBCQW1HQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IENvbnN0cnVjdCB9IGZyb20gXCJjb25zdHJ1Y3RzXCI7XG5pbXBvcnQgKiBhcyBzMyBmcm9tIFwiYXdzLWNkay1saWIvYXdzLXMzXCI7XG5pbXBvcnQgKiBhcyBjbG91ZGZvbnQgZnJvbSBcImF3cy1jZGstbGliL2F3cy1jbG91ZGZyb250XCI7XG5pbXBvcnQgKiBhcyBvcmlnaW5zIGZyb20gXCJhd3MtY2RrLWxpYi9hd3MtY2xvdWRmcm9udC1vcmlnaW5zXCI7XG5pbXBvcnQgKiBhcyBjZXJ0aWZpY2F0ZW1hbmFnZXIgZnJvbSBcImF3cy1jZGstbGliL2F3cy1jZXJ0aWZpY2F0ZW1hbmFnZXJcIjtcbmltcG9ydCAqIGFzIGNkayBmcm9tIFwiYXdzLWNkay1saWJcIjtcbmltcG9ydCAqIGFzIGlhbSBmcm9tIFwiYXdzLWNkay1saWIvYXdzLWlhbVwiO1xuaW1wb3J0ICogYXMgcm91dGU1MyBmcm9tIFwiYXdzLWNkay1saWIvYXdzLXJvdXRlNTNcIjtcblxuZXhwb3J0IGludGVyZmFjZSBEb21haW5Db25maWcge1xuICAvKiogVGhlIHJvb3QgZG9tYWluIG5hbWUgKGUuZy4sIGV4YW1wbGUuY29tKS5cbiAgICogVGhlcmUgbXVzdCBiZSBhbiBhc3NvY2lhdGVkIGhvc3RlZCB6b25lIGluIFJvdXRlIDUzIGZvciB0aGlzIGRvbWFpbi5cbiAgICovXG4gIGRvbWFpbk5hbWU6IHN0cmluZztcbiAgLyoqIFRoZSBzdWJkb21haW4gbmFtZSAqL1xuICBzdWJkb21haW5OYW1lOiBzdHJpbmc7XG4gIC8qKiBUaGUgQVJOIG9mIHRoZSBTU0wgY2VydGlmaWNhdGUgdG8gdXNlIGZvciB0aGUgZG9tYWluLiAqL1xuICBjZXJ0aWZpY2F0ZUFybjogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFdlYnNpdGVQcm9wcyB7XG4gIC8qKiBUaGUgbmFtZSBvZiB0aGUgUzMgYnVja2V0IHRoYXQgd2lsbCBob3N0IHRoZSB3ZWJzaXRlIGNvbnRlbnQuICovXG4gIGJ1Y2tldE5hbWU6IHN0cmluZztcblxuICAvKiogVGhlIHBhdGggdG8gdGhlIGluZGV4IGRvY3VtZW50IHRoYXQgd2lsbCBiZSBzZXJ2ZWQgYXMgdGhlIGRlZmF1bHQgcGFnZS4gKi9cbiAgaW5kZXhGaWxlOiBzdHJpbmc7XG5cbiAgLyoqIFRoZSBwYXRoIHRvIHRoZSBlcnJvciBkb2N1bWVudCB0aGF0IHdpbGwgYmUgc2VydmVkIHdoZW4gYW4gZXJyb3Igb2NjdXJzLiAqL1xuICBlcnJvckZpbGU6IHN0cmluZztcblxuICAvKiogT3B0aW9uYWwgY29uZmlndXJhdGlvbiBmb3IgY3VzdG9tIGRvbWFpbiBzZXR1cC4gKi9cbiAgZG9tYWluQ29uZmlnPzogRG9tYWluQ29uZmlnO1xuXG4gIC8qKiBPcHRpb25hbCBwYXRoIHRvIGEgY3VzdG9tIDQwNCBwYWdlLiBJZiBub3Qgc3BlY2lmaWVkLCB0aGUgZXJyb3IgZmlsZSB3aWxsIGJlIHVzZWQuICovXG4gIG5vdEZvdW5kUmVzcG9uc2VQYWdlUGF0aD86IHN0cmluZztcbn1cblxuZXhwb3J0IGNsYXNzIFdlYnNpdGUgZXh0ZW5kcyBDb25zdHJ1Y3Qge1xuICBjb25zdHJ1Y3RvcihzY29wZTogQ29uc3RydWN0LCBpZDogc3RyaW5nLCBwcm9wczogV2Vic2l0ZVByb3BzKSB7XG4gICAgc3VwZXIoc2NvcGUsIGlkKTtcbiAgICBjb25zdCB3ZWJCdWNrZXQgPSBuZXcgczMuQnVja2V0KHRoaXMsIHByb3BzLmJ1Y2tldE5hbWUsIHtcbiAgICAgIHdlYnNpdGVJbmRleERvY3VtZW50OiBwcm9wcy5pbmRleEZpbGUsXG4gICAgICB3ZWJzaXRlRXJyb3JEb2N1bWVudDogcHJvcHMuZXJyb3JGaWxlLFxuICAgICAgcmVtb3ZhbFBvbGljeTogY2RrLlJlbW92YWxQb2xpY3kuREVTVFJPWSxcbiAgICAgIGVuY3J5cHRpb246IHMzLkJ1Y2tldEVuY3J5cHRpb24uUzNfTUFOQUdFRCxcbiAgICB9KTtcbiAgICBjb25zdCBvYWkgPSBuZXcgY2xvdWRmb250Lk9yaWdpbkFjY2Vzc0lkZW50aXR5KFxuICAgICAgdGhpcyxcbiAgICAgIGAke3Byb3BzLmJ1Y2tldE5hbWV9LU9BSWAsXG4gICAgKTtcbiAgICB3ZWJCdWNrZXQuYWRkVG9SZXNvdXJjZVBvbGljeShcbiAgICAgIG5ldyBpYW0uUG9saWN5U3RhdGVtZW50KHtcbiAgICAgICAgYWN0aW9uczogW1wiczM6R2V0T2JqZWN0XCJdLFxuICAgICAgICByZXNvdXJjZXM6IFt3ZWJCdWNrZXQuYXJuRm9yT2JqZWN0cyhcIipcIildLFxuICAgICAgICBwcmluY2lwYWxzOiBbXG4gICAgICAgICAgbmV3IGlhbS5DYW5vbmljYWxVc2VyUHJpbmNpcGFsKFxuICAgICAgICAgICAgb2FpLmNsb3VkRnJvbnRPcmlnaW5BY2Nlc3NJZGVudGl0eVMzQ2Fub25pY2FsVXNlcklkLFxuICAgICAgICAgICksXG4gICAgICAgIF0sXG4gICAgICB9KSxcbiAgICApO1xuXG4gICAgY29uc3QgZGlzdHJpYnV0aW9uID0gbmV3IGNsb3VkZm9udC5EaXN0cmlidXRpb24oXG4gICAgICB0aGlzLFxuICAgICAgYCR7cHJvcHMuYnVja2V0TmFtZX0tZGlzdHJpYnV0aW9uYCxcbiAgICAgIHtcbiAgICAgICAgZGVmYXVsdEJlaGF2aW9yOiB7XG4gICAgICAgICAgb3JpZ2luOiBuZXcgb3JpZ2lucy5TM1N0YXRpY1dlYnNpdGVPcmlnaW4od2ViQnVja2V0KSxcbiAgICAgICAgICB2aWV3ZXJQcm90b2NvbFBvbGljeTpcbiAgICAgICAgICAgIGNsb3VkZm9udC5WaWV3ZXJQcm90b2NvbFBvbGljeS5SRURJUkVDVF9UT19IVFRQUyxcbiAgICAgICAgfSxcbiAgICAgICAgZXJyb3JSZXNwb25zZXM6IFtcbiAgICAgICAgICB7XG4gICAgICAgICAgICBodHRwU3RhdHVzOiA0MDQsXG4gICAgICAgICAgICByZXNwb25zZUh0dHBTdGF0dXM6IDQwNCxcbiAgICAgICAgICAgIHJlc3BvbnNlUGFnZVBhdGg6IHByb3BzLm5vdEZvdW5kUmVzcG9uc2VQYWdlUGF0aCB8fCBgLzQwNC5odG1sYCxcbiAgICAgICAgICAgIHR0bDogY2RrLkR1cmF0aW9uLm1pbnV0ZXMoMzApLFxuICAgICAgICAgIH0sXG4gICAgICAgIF0sXG4gICAgICAgIHByaWNlQ2xhc3M6IGNsb3VkZm9udC5QcmljZUNsYXNzLlBSSUNFX0NMQVNTXzEwMCxcbiAgICAgICAgLi4uKHByb3BzLmRvbWFpbkNvbmZpZ1xuICAgICAgICAgID8ge1xuICAgICAgICAgICAgICBkb21haW5OYW1lczogW3RoaXMuX2dldEZ1bGxEb21haW5OYW1lKHByb3BzLmRvbWFpbkNvbmZpZyldLFxuICAgICAgICAgICAgICBjZXJ0aWZpY2F0ZTogdGhpcy5fZ2V0Q2VydGlmaWNhdGUoXG4gICAgICAgICAgICAgICAgcHJvcHMuZG9tYWluQ29uZmlnLmNlcnRpZmljYXRlQXJuLFxuICAgICAgICAgICAgICApLFxuICAgICAgICAgICAgfVxuICAgICAgICAgIDoge30pLFxuICAgICAgfSxcbiAgICApO1xuXG4gICAgaWYgKHByb3BzLmRvbWFpbkNvbmZpZykge1xuICAgICAgY29uc3QgaG9zdGVkWm9uZSA9IHJvdXRlNTMuSG9zdGVkWm9uZS5mcm9tTG9va3VwKHRoaXMsIFwiSG9zdGVkWm9uZVwiLCB7XG4gICAgICAgIGRvbWFpbk5hbWU6IHByb3BzLmRvbWFpbkNvbmZpZy5kb21haW5OYW1lLFxuICAgICAgfSk7XG4gICAgICBjb25zdCBkb21haW5BUmVjb3JkID0gbmV3IHJvdXRlNTMuQVJlY29yZCh0aGlzLCBcIkRvbWFpbkFSZWNvcmRcIiwge1xuICAgICAgICB6b25lOiBob3N0ZWRab25lLFxuICAgICAgICByZWNvcmROYW1lOiB0aGlzLl9nZXRGdWxsRG9tYWluTmFtZShwcm9wcy5kb21haW5Db25maWcpLFxuICAgICAgICB0YXJnZXQ6IGNkay5hd3Nfcm91dGU1My5SZWNvcmRUYXJnZXQuZnJvbUFsaWFzKFxuICAgICAgICAgIG5ldyBjZGsuYXdzX3JvdXRlNTNfdGFyZ2V0cy5DbG91ZEZyb250VGFyZ2V0KGRpc3RyaWJ1dGlvbiksXG4gICAgICAgICksXG4gICAgICB9KTtcbiAgICAgIGRvbWFpbkFSZWNvcmQubm9kZS5hZGREZXBlbmRlbmN5KGRpc3RyaWJ1dGlvbik7XG4gICAgfVxuXG4gICAgbmV3IGNkay5DZm5PdXRwdXQodGhpcywgXCJjbG91ZGZyb250LXdlYnNpdGUtdXJsXCIsIHtcbiAgICAgIHZhbHVlOiBkaXN0cmlidXRpb24uZGlzdHJpYnV0aW9uRG9tYWluTmFtZSxcbiAgICAgIGRlc2NyaXB0aW9uOiBcIkNsb3VkRnJvbnQgRGlzdHJpYnV0aW9uIERvbWFpbiBOYW1lXCIsXG4gICAgfSk7XG5cbiAgICBuZXcgY2RrLkNmbk91dHB1dCh0aGlzLCBcInMzLXdlYnNpdGUtdXJsXCIsIHtcbiAgICAgIHZhbHVlOiB3ZWJCdWNrZXQuYnVja2V0V2Vic2l0ZVVybCxcbiAgICAgIGRlc2NyaXB0aW9uOiBcIlMzIEJ1Y2tldCBXZWJzaXRlIFVSTFwiLFxuICAgIH0pO1xuXG4gICAgaWYgKHByb3BzLmRvbWFpbkNvbmZpZykge1xuICAgICAgbmV3IGNkay5DZm5PdXRwdXQodGhpcywgXCJ3ZWJzaXRlLXVybFwiLCB7XG4gICAgICAgIHZhbHVlOiBkaXN0cmlidXRpb24uZG9tYWluTmFtZSxcbiAgICAgICAgZGVzY3JpcHRpb246IFwiV2Vic2l0ZSBVUkxcIixcbiAgICAgIH0pO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgX2dldEZ1bGxEb21haW5OYW1lKGRvbWFpbkNvbmZpZzogRG9tYWluQ29uZmlnKTogc3RyaW5nIHtcbiAgICByZXR1cm4gZG9tYWluQ29uZmlnLnN1YmRvbWFpbk5hbWVcbiAgICAgID8gYCR7ZG9tYWluQ29uZmlnLnN1YmRvbWFpbk5hbWV9LiR7ZG9tYWluQ29uZmlnLmRvbWFpbk5hbWV9YFxuICAgICAgOiBkb21haW5Db25maWcuZG9tYWluTmFtZTtcbiAgfVxuXG4gIHByaXZhdGUgX2dldENlcnRpZmljYXRlKGFybjogc3RyaW5nKTogY2VydGlmaWNhdGVtYW5hZ2VyLklDZXJ0aWZpY2F0ZSB7XG4gICAgcmV0dXJuIGNlcnRpZmljYXRlbWFuYWdlci5DZXJ0aWZpY2F0ZS5mcm9tQ2VydGlmaWNhdGVBcm4oXG4gICAgICB0aGlzLFxuICAgICAgYHdlYnNpdGUtY2VydGAsXG4gICAgICBhcm4sXG4gICAgKTtcbiAgfVxufVxuIl19
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import * as s3 from "aws-cdk-lib/aws-s3";
|
|
3
|
+
import * as cloudfont from "aws-cdk-lib/aws-cloudfront";
|
|
4
|
+
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
|
|
5
|
+
import * as certificatemanager from "aws-cdk-lib/aws-certificatemanager";
|
|
6
|
+
import * as cdk from "aws-cdk-lib";
|
|
7
|
+
import * as iam from "aws-cdk-lib/aws-iam";
|
|
8
|
+
import * as route53 from "aws-cdk-lib/aws-route53";
|
|
9
|
+
|
|
10
|
+
export interface DomainConfig {
|
|
11
|
+
/** The root domain name (e.g., example.com).
|
|
12
|
+
* There must be an associated hosted zone in Route 53 for this domain.
|
|
13
|
+
*/
|
|
14
|
+
domainName: string;
|
|
15
|
+
/** The subdomain name */
|
|
16
|
+
subdomainName: string;
|
|
17
|
+
/** The ARN of the SSL certificate to use for the domain. */
|
|
18
|
+
certificateArn: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WebsiteProps {
|
|
22
|
+
/** The name of the S3 bucket that will host the website content. */
|
|
23
|
+
bucketName: string;
|
|
24
|
+
|
|
25
|
+
/** The path to the index document that will be served as the default page. */
|
|
26
|
+
indexFile: string;
|
|
27
|
+
|
|
28
|
+
/** The path to the error document that will be served when an error occurs. */
|
|
29
|
+
errorFile: string;
|
|
30
|
+
|
|
31
|
+
/** Optional configuration for custom domain setup. */
|
|
32
|
+
domainConfig?: DomainConfig;
|
|
33
|
+
|
|
34
|
+
/** Optional path to a custom 404 page. If not specified, the error file will be used. */
|
|
35
|
+
notFoundResponsePagePath?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class Website extends Construct {
|
|
39
|
+
constructor(scope: Construct, id: string, props: WebsiteProps) {
|
|
40
|
+
super(scope, id);
|
|
41
|
+
const webBucket = new s3.Bucket(this, props.bucketName, {
|
|
42
|
+
websiteIndexDocument: props.indexFile,
|
|
43
|
+
websiteErrorDocument: props.errorFile,
|
|
44
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
45
|
+
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
46
|
+
});
|
|
47
|
+
const oai = new cloudfont.OriginAccessIdentity(
|
|
48
|
+
this,
|
|
49
|
+
`${props.bucketName}-OAI`,
|
|
50
|
+
);
|
|
51
|
+
webBucket.addToResourcePolicy(
|
|
52
|
+
new iam.PolicyStatement({
|
|
53
|
+
actions: ["s3:GetObject"],
|
|
54
|
+
resources: [webBucket.arnForObjects("*")],
|
|
55
|
+
principals: [
|
|
56
|
+
new iam.CanonicalUserPrincipal(
|
|
57
|
+
oai.cloudFrontOriginAccessIdentityS3CanonicalUserId,
|
|
58
|
+
),
|
|
59
|
+
],
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const distribution = new cloudfont.Distribution(
|
|
64
|
+
this,
|
|
65
|
+
`${props.bucketName}-distribution`,
|
|
66
|
+
{
|
|
67
|
+
defaultBehavior: {
|
|
68
|
+
origin: new origins.S3StaticWebsiteOrigin(webBucket),
|
|
69
|
+
viewerProtocolPolicy:
|
|
70
|
+
cloudfont.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
71
|
+
},
|
|
72
|
+
errorResponses: [
|
|
73
|
+
{
|
|
74
|
+
httpStatus: 404,
|
|
75
|
+
responseHttpStatus: 404,
|
|
76
|
+
responsePagePath: props.notFoundResponsePagePath || `/404.html`,
|
|
77
|
+
ttl: cdk.Duration.minutes(30),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
priceClass: cloudfont.PriceClass.PRICE_CLASS_100,
|
|
81
|
+
...(props.domainConfig
|
|
82
|
+
? {
|
|
83
|
+
domainNames: [this._getFullDomainName(props.domainConfig)],
|
|
84
|
+
certificate: this._getCertificate(
|
|
85
|
+
props.domainConfig.certificateArn,
|
|
86
|
+
),
|
|
87
|
+
}
|
|
88
|
+
: {}),
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (props.domainConfig) {
|
|
93
|
+
const hostedZone = route53.HostedZone.fromLookup(this, "HostedZone", {
|
|
94
|
+
domainName: props.domainConfig.domainName,
|
|
95
|
+
});
|
|
96
|
+
const domainARecord = new route53.ARecord(this, "DomainARecord", {
|
|
97
|
+
zone: hostedZone,
|
|
98
|
+
recordName: this._getFullDomainName(props.domainConfig),
|
|
99
|
+
target: cdk.aws_route53.RecordTarget.fromAlias(
|
|
100
|
+
new cdk.aws_route53_targets.CloudFrontTarget(distribution),
|
|
101
|
+
),
|
|
102
|
+
});
|
|
103
|
+
domainARecord.node.addDependency(distribution);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
new cdk.CfnOutput(this, "cloudfront-website-url", {
|
|
107
|
+
value: distribution.distributionDomainName,
|
|
108
|
+
description: "CloudFront Distribution Domain Name",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
new cdk.CfnOutput(this, "s3-website-url", {
|
|
112
|
+
value: webBucket.bucketWebsiteUrl,
|
|
113
|
+
description: "S3 Bucket Website URL",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (props.domainConfig) {
|
|
117
|
+
new cdk.CfnOutput(this, "website-url", {
|
|
118
|
+
value: distribution.domainName,
|
|
119
|
+
description: "Website URL",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private _getFullDomainName(domainConfig: DomainConfig): string {
|
|
125
|
+
return domainConfig.subdomainName
|
|
126
|
+
? `${domainConfig.subdomainName}.${domainConfig.domainName}`
|
|
127
|
+
: domainConfig.domainName;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private _getCertificate(arn: string): certificatemanager.ICertificate {
|
|
131
|
+
return certificatemanager.Certificate.fromCertificateArn(
|
|
132
|
+
this,
|
|
133
|
+
`website-cert`,
|
|
134
|
+
arn,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sitblueprint/website-construct",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "A reusable AWS CDK construct for deploying static websites with optional custom domain support.",
|
|
5
5
|
"author": "Miguel Merlin <mmerlin@stevens.edu>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,4 +43,4 @@
|
|
|
43
43
|
"aws-cdk-lib": "2.208.0",
|
|
44
44
|
"constructs": "^10.0.0"
|
|
45
45
|
}
|
|
46
|
-
}
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
const assertions_1 = require("aws-cdk-lib/assertions");
|
|
27
|
+
const cdk = __importStar(require("aws-cdk-lib"));
|
|
28
|
+
const lib_1 = require("../lib");
|
|
29
|
+
describe("Website", () => {
|
|
30
|
+
let app;
|
|
31
|
+
let stack;
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
app = new cdk.App();
|
|
34
|
+
stack = new cdk.Stack(app, "TestStack", {
|
|
35
|
+
env: { account: "123456789012", region: "us-east-1" },
|
|
36
|
+
});
|
|
37
|
+
stack.node.setContext("hosted-zone:account=123456789012:domainName=example.com:region=us-east-1", {
|
|
38
|
+
Id: "/hostedzone/Z123456789012",
|
|
39
|
+
Name: "example.com.",
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("Basic functionality", () => {
|
|
43
|
+
test("creates S3 bucket with basic configuration", () => {
|
|
44
|
+
const props = {
|
|
45
|
+
bucketName: "test-website-bucket",
|
|
46
|
+
indexFile: "index.html",
|
|
47
|
+
errorFile: "error.html",
|
|
48
|
+
};
|
|
49
|
+
new lib_1.Website(stack, "TestWebsite", props);
|
|
50
|
+
const template = assertions_1.Template.fromStack(stack);
|
|
51
|
+
template.hasResourceProperties("AWS::S3::Bucket", {
|
|
52
|
+
WebsiteConfiguration: {
|
|
53
|
+
IndexDocument: "index.html",
|
|
54
|
+
ErrorDocument: "error.html",
|
|
55
|
+
},
|
|
56
|
+
BucketEncryption: {
|
|
57
|
+
ServerSideEncryptionConfiguration: [
|
|
58
|
+
{
|
|
59
|
+
ServerSideEncryptionByDefault: {
|
|
60
|
+
SSEAlgorithm: "AES256",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
test("creates CloudFront Origin Access Identity", () => {
|
|
68
|
+
const props = {
|
|
69
|
+
bucketName: "test-website-bucket",
|
|
70
|
+
indexFile: "index.html",
|
|
71
|
+
errorFile: "error.html",
|
|
72
|
+
};
|
|
73
|
+
new lib_1.Website(stack, "TestWebsite", props);
|
|
74
|
+
const template = assertions_1.Template.fromStack(stack);
|
|
75
|
+
template.hasResourceProperties("AWS::CloudFront::CloudFrontOriginAccessIdentity", {
|
|
76
|
+
CloudFrontOriginAccessIdentityConfig: {
|
|
77
|
+
Comment: assertions_1.Match.anyValue(),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
test("creates CloudFront distribution with correct configuration", () => {
|
|
82
|
+
const props = {
|
|
83
|
+
bucketName: "test-website-bucket",
|
|
84
|
+
indexFile: "index.html",
|
|
85
|
+
errorFile: "error.html",
|
|
86
|
+
};
|
|
87
|
+
new lib_1.Website(stack, "TestWebsite", props);
|
|
88
|
+
const template = assertions_1.Template.fromStack(stack);
|
|
89
|
+
template.hasResourceProperties("AWS::CloudFront::Distribution", {
|
|
90
|
+
DistributionConfig: {
|
|
91
|
+
DefaultCacheBehavior: {
|
|
92
|
+
ViewerProtocolPolicy: "redirect-to-https",
|
|
93
|
+
},
|
|
94
|
+
CustomErrorResponses: [
|
|
95
|
+
{
|
|
96
|
+
ErrorCode: 404,
|
|
97
|
+
ResponseCode: 404,
|
|
98
|
+
ResponsePagePath: "/404.html",
|
|
99
|
+
ErrorCachingMinTTL: 1800,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
PriceClass: "PriceClass_100",
|
|
103
|
+
Enabled: true,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe("Custom error page configuration", () => {
|
|
109
|
+
test("uses custom not found response page path", () => {
|
|
110
|
+
const props = {
|
|
111
|
+
bucketName: "test-website-bucket",
|
|
112
|
+
indexFile: "index.html",
|
|
113
|
+
errorFile: "error.html",
|
|
114
|
+
notFoundResponsePagePath: "/custom-404.html",
|
|
115
|
+
};
|
|
116
|
+
new lib_1.Website(stack, "TestWebsite", props);
|
|
117
|
+
const template = assertions_1.Template.fromStack(stack);
|
|
118
|
+
template.hasResourceProperties("AWS::CloudFront::Distribution", {
|
|
119
|
+
DistributionConfig: {
|
|
120
|
+
CustomErrorResponses: [
|
|
121
|
+
{
|
|
122
|
+
ErrorCode: 404,
|
|
123
|
+
ResponseCode: 404,
|
|
124
|
+
ResponsePagePath: "/custom-404.html",
|
|
125
|
+
ErrorCachingMinTTL: 1800,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
test("uses default 404.html when notFoundResponsePagePath is not provided", () => {
|
|
132
|
+
const props = {
|
|
133
|
+
bucketName: "test-website-bucket",
|
|
134
|
+
indexFile: "index.html",
|
|
135
|
+
errorFile: "error.html",
|
|
136
|
+
};
|
|
137
|
+
new lib_1.Website(stack, "TestWebsite", props);
|
|
138
|
+
const template = assertions_1.Template.fromStack(stack);
|
|
139
|
+
template.hasResourceProperties("AWS::CloudFront::Distribution", {
|
|
140
|
+
DistributionConfig: {
|
|
141
|
+
CustomErrorResponses: [
|
|
142
|
+
{
|
|
143
|
+
ErrorCode: 404,
|
|
144
|
+
ResponseCode: 404,
|
|
145
|
+
ResponsePagePath: "/404.html",
|
|
146
|
+
ErrorCachingMinTTL: 1800,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe("Domain configuration", () => {
|
|
154
|
+
const domainConfig = {
|
|
155
|
+
domainName: "example.com",
|
|
156
|
+
subdomainName: "www",
|
|
157
|
+
certificateArn: "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
|
|
158
|
+
};
|
|
159
|
+
test("configures CloudFront distribution with custom domain", () => {
|
|
160
|
+
const props = {
|
|
161
|
+
bucketName: "test-website-bucket",
|
|
162
|
+
indexFile: "index.html",
|
|
163
|
+
errorFile: "error.html",
|
|
164
|
+
domainConfig,
|
|
165
|
+
};
|
|
166
|
+
new lib_1.Website(stack, "TestWebsite", props);
|
|
167
|
+
const template = assertions_1.Template.fromStack(stack);
|
|
168
|
+
template.hasResourceProperties("AWS::CloudFront::Distribution", {
|
|
169
|
+
DistributionConfig: {
|
|
170
|
+
Aliases: ["www.example.com"],
|
|
171
|
+
ViewerCertificate: {
|
|
172
|
+
AcmCertificateArn: domainConfig.certificateArn,
|
|
173
|
+
SslSupportMethod: "sni-only",
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
test("creates Route53 A record when domain config is provided", () => {
|
|
179
|
+
const props = {
|
|
180
|
+
bucketName: "test-website-bucket",
|
|
181
|
+
indexFile: "index.html",
|
|
182
|
+
errorFile: "error.html",
|
|
183
|
+
domainConfig,
|
|
184
|
+
};
|
|
185
|
+
new lib_1.Website(stack, "TestWebsite", props);
|
|
186
|
+
const template = assertions_1.Template.fromStack(stack);
|
|
187
|
+
template.hasResourceProperties("AWS::Route53::RecordSet", {
|
|
188
|
+
Type: "A",
|
|
189
|
+
Name: "www.example.com.",
|
|
190
|
+
AliasTarget: {
|
|
191
|
+
DNSName: assertions_1.Match.anyValue(),
|
|
192
|
+
HostedZoneId: assertions_1.Match.anyValue(),
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
test("handles domain without subdomain", () => {
|
|
197
|
+
const domainConfigWithoutSub = {
|
|
198
|
+
domainName: "example.com",
|
|
199
|
+
subdomainName: "",
|
|
200
|
+
certificateArn: "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
|
|
201
|
+
};
|
|
202
|
+
const props = {
|
|
203
|
+
bucketName: "test-website-bucket",
|
|
204
|
+
indexFile: "index.html",
|
|
205
|
+
errorFile: "error.html",
|
|
206
|
+
domainConfig: domainConfigWithoutSub,
|
|
207
|
+
};
|
|
208
|
+
new lib_1.Website(stack, "TestWebsite", props);
|
|
209
|
+
const template = assertions_1.Template.fromStack(stack);
|
|
210
|
+
template.hasResourceProperties("AWS::CloudFront::Distribution", {
|
|
211
|
+
DistributionConfig: {
|
|
212
|
+
Aliases: ["example.com"],
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
template.hasResourceProperties("AWS::Route53::RecordSet", {
|
|
216
|
+
Type: "A",
|
|
217
|
+
Name: "example.com.",
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe("Private methods", () => {
|
|
222
|
+
test("_getFullDomainName returns correct domain with subdomain", () => {
|
|
223
|
+
const props = {
|
|
224
|
+
bucketName: "test-bucket",
|
|
225
|
+
indexFile: "index.html",
|
|
226
|
+
errorFile: "error.html",
|
|
227
|
+
};
|
|
228
|
+
const construct = new lib_1.Website(stack, "TestWebsite", props);
|
|
229
|
+
// Access private method through bracket notation for testing
|
|
230
|
+
const getFullDomainName = construct._getFullDomainName;
|
|
231
|
+
const domainConfig = {
|
|
232
|
+
domainName: "example.com",
|
|
233
|
+
subdomainName: "blog",
|
|
234
|
+
certificateArn: "arn:test",
|
|
235
|
+
};
|
|
236
|
+
const result = getFullDomainName(domainConfig);
|
|
237
|
+
expect(result).toBe("blog.example.com");
|
|
238
|
+
});
|
|
239
|
+
test("_getFullDomainName returns root domain when subdomain is empty", () => {
|
|
240
|
+
const props = {
|
|
241
|
+
bucketName: "test-bucket",
|
|
242
|
+
indexFile: "index.html",
|
|
243
|
+
errorFile: "error.html",
|
|
244
|
+
};
|
|
245
|
+
const construct = new lib_1.Website(stack, "TestWebsite", props);
|
|
246
|
+
const getFullDomainName = construct._getFullDomainName;
|
|
247
|
+
const domainConfig = {
|
|
248
|
+
domainName: "example.com",
|
|
249
|
+
subdomainName: "",
|
|
250
|
+
certificateArn: "arn:test",
|
|
251
|
+
};
|
|
252
|
+
const result = getFullDomainName(domainConfig);
|
|
253
|
+
expect(result).toBe("example.com");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
describe("Edge cases", () => {
|
|
257
|
+
test("handles minimal configuration", () => {
|
|
258
|
+
const props = {
|
|
259
|
+
bucketName: "minimal-bucket",
|
|
260
|
+
indexFile: "index.html",
|
|
261
|
+
errorFile: "error.html",
|
|
262
|
+
};
|
|
263
|
+
expect(() => {
|
|
264
|
+
new lib_1.Website(stack, "TestWebsite", props);
|
|
265
|
+
}).not.toThrow();
|
|
266
|
+
const template = assertions_1.Template.fromStack(stack);
|
|
267
|
+
template.resourceCountIs("AWS::S3::Bucket", 1);
|
|
268
|
+
template.resourceCountIs("AWS::CloudFront::Distribution", 1);
|
|
269
|
+
template.resourceCountIs("AWS::CloudFront::CloudFrontOriginAccessIdentity", 1);
|
|
270
|
+
});
|
|
271
|
+
test("does not create Route53 resources when domain config is not provided", () => {
|
|
272
|
+
const props = {
|
|
273
|
+
bucketName: "test-bucket",
|
|
274
|
+
indexFile: "index.html",
|
|
275
|
+
errorFile: "error.html",
|
|
276
|
+
};
|
|
277
|
+
new lib_1.Website(stack, "TestWebsite", props);
|
|
278
|
+
const template = assertions_1.Template.fromStack(stack);
|
|
279
|
+
template.resourceCountIs("AWS::Route53::RecordSet", 0);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2Vic2l0ZS1jb25zdHJ1Y3QudGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIndlYnNpdGUtY29uc3RydWN0LnRlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQUFBLHVEQUF5RDtBQUN6RCxpREFBbUM7QUFDbkMsZ0NBQTZEO0FBRTdELFFBQVEsQ0FBQyxTQUFTLEVBQUUsR0FBRyxFQUFFO0lBQ3ZCLElBQUksR0FBWSxDQUFDO0lBQ2pCLElBQUksS0FBZ0IsQ0FBQztJQUVyQixVQUFVLENBQUMsR0FBRyxFQUFFO1FBQ2QsR0FBRyxHQUFHLElBQUksR0FBRyxDQUFDLEdBQUcsRUFBRSxDQUFDO1FBQ3BCLEtBQUssR0FBRyxJQUFJLEdBQUcsQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFLFdBQVcsRUFBRTtZQUN0QyxHQUFHLEVBQUUsRUFBRSxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sRUFBRSxXQUFXLEVBQUU7U0FDdEQsQ0FBQyxDQUFDO1FBQ0gsS0FBSyxDQUFDLElBQUksQ0FBQyxVQUFVLENBQ25CLDBFQUEwRSxFQUMxRTtZQUNFLEVBQUUsRUFBRSwyQkFBMkI7WUFDL0IsSUFBSSxFQUFFLGNBQWM7U0FDckIsQ0FDRixDQUFDO0lBQ0osQ0FBQyxDQUFDLENBQUM7SUFFSCxRQUFRLENBQUMscUJBQXFCLEVBQUUsR0FBRyxFQUFFO1FBQ25DLElBQUksQ0FBQyw0Q0FBNEMsRUFBRSxHQUFHLEVBQUU7WUFDdEQsTUFBTSxLQUFLLEdBQWlCO2dCQUMxQixVQUFVLEVBQUUscUJBQXFCO2dCQUNqQyxTQUFTLEVBQUUsWUFBWTtnQkFDdkIsU0FBUyxFQUFFLFlBQVk7YUFDeEIsQ0FBQztZQUVGLElBQUksYUFBTyxDQUFDLEtBQUssRUFBRSxhQUFhLEVBQUUsS0FBSyxDQUFDLENBQUM7WUFFekMsTUFBTSxRQUFRLEdBQUcscUJBQVEsQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDLENBQUM7WUFFM0Ms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Q0FBQyxpQkFBaUIsRUFBRSxDQUFDLENBQUMsQ0FBQztZQUMvQyxRQUFRLENBQUMsZUFBZSxDQUFDLCtCQUErQixFQUFFLENBQUMsQ0FBQyxDQUFDO1lBQzdELFFBQVEsQ0FBQyxlQUFlLENBQ3RCLGlEQUFpRCxFQUNqRCxDQUFDLENBQ0YsQ0FBQztRQUNKLENBQUMsQ0FBQyxDQUFDO1FBRUgsSUFBSSxDQUFDLHNFQUFzRSxFQUFFLEdBQUcsRUFBRTtZQUNoRixNQUFNLEtBQUssR0FBaUI7Z0JBQzFCLFVBQVUsRUFBRSxhQUFhO2dCQUN6QixTQUFTLEVBQUUsWUFBWTtnQkFDdkIsU0FBUyxFQUFFLFlBQVk7YUFDeEIsQ0FBQztZQUVGLElBQUksYUFBTyxDQUFDLEtBQUssRUFBRSxhQUFhLEVBQUUsS0FBSyxDQUFDLENBQUM7WUFFekMsTUFBTSxRQUFRLEdBQUcscUJBQVEsQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDLENBQUM7WUFDM0MsUUFBUSxDQUFDLGVBQWUsQ0FBQyx5QkFBeUIsRUFBRSxDQUFDLENBQUMsQ0FBQztRQUN6RCxDQUFDLENBQUMsQ0FBQztJQUNMLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBUZW1wbGF0ZSwgTWF0Y2ggfSBmcm9tIFwiYXdzLWNkay1saWIvYXNzZXJ0aW9uc1wiO1xuaW1wb3J0ICogYXMgY2RrIGZyb20gXCJhd3MtY2RrLWxpYlwiO1xuaW1wb3J0IHsgV2Vic2l0ZSwgV2Vic2l0ZVByb3BzLCBEb21haW5Db25maWcgfSBmcm9tIFwiLi4vbGliXCI7XG5cbmRlc2NyaWJlKFwiV2Vic2l0ZVwiLCAoKSA9PiB7XG4gIGxldCBhcHA6IGNkay5BcHA7XG4gIGxldCBzdGFjazogY2RrLlN0YWNrO1xuXG4gIGJlZm9yZUVhY2goKCkgPT4ge1xuICAgIGFwcCA9IG5ldyBjZGsuQXBwKCk7XG4gICAgc3RhY2sgPSBuZXcgY2RrLlN0YWNrKGFwcCwgXCJUZXN0U3RhY2tcIiwge1xuICAgICAgZW52OiB7IGFjY291bnQ6IFwiMTIzNDU2Nzg5MDEyXCIsIHJlZ2lvbjogXCJ1cy1lYXN0LTFcIiB9LFxuICAgIH0pO1xuICAgIHN0YWNrLm5vZGUuc2V0Q29udGV4dChcbiAgICAgIFwiaG9zdGVkLXpvbmU6YWNjb3VudD0xMjM0NTY3ODkwMTI6ZG9tYWluTmFtZT1leGFtcGxlLmNvbTpyZWdpb249dXMtZWFzdC0xXCIsXG4gICAgICB7XG4gICAgICAgIElkOiBcIi9ob3N0ZWR6b25lL1oxMjM0NTY3ODkwMTJcIixcbiAgICAgICAgTmFtZTogXCJleGFtcGxlLmNvbS5cIixcbiAgICAgIH0sXG4gICAgKTtcbiAgfSk7XG5cbiAgZGVzY3JpYmUoXCJCYXNpYyBmdW5jdGlvbmFsaXR5XCIsICgpID0+IHtcbiAgICB0ZXN0KFwiY3JlYXRlcyBTMyBidWNrZXQgd2l0aCBiYXNpYyBjb25maWd1cmF0aW9uXCIsICgpID0+IHtcbiAgICAgIGNvbnN0IHByb3BzOiBXZWJzaXRlUHJvcHMgPSB7XG4gICAgICAgIGJ1Y2tldE5hbWU6IFwidGVzdC13ZWJzaXRlLWJ1Y2tldFwiLFxuICAgICAgICBpbmRleEZpbGU6IFwiaW5kZXguaHRtbFwiLFxuICAgICAgICBlcnJvckZpbGU6IFwiZXJyb3IuaHRtbFwiLFxuICAgICAgfTtcblxuICAgICAgbmV3IFdlYnNpdGUoc3RhY2ssIFwiVGVzdFdlYnNpdGVcIiwgcHJvcHMpO1xuXG4gICAgICBjb25zdCB0ZW1wbGF0ZSA9IFRlbXBsYXRlLmZyb21TdGFjayhzdGFjayk7XG5cbiAgICAgIHRlbXBsYXRlLmhhc1Jlc291cmNlUHJvcGVydGllcyhcIkFXUzo6UzM6OkJ1Y2tldFwiLCB7XG4gICAgICAgIFdlYnNpdGVDb25maWd1cmF0aW9uOiB7XG4gICAgICAgICAgSW5kZXhEb2N1bWVudDogXCJpbmRleC5odG1sXCIsXG4gICAgICAgICAgRXJyb3JEb2N1bWVudDogXCJlcnJvci5odG1sXCIsXG4gICAgICAgIH0sXG4gICAgICAgIEJ1Y2tldEVuY3J5cHRpb246IHtcbiAgICAgICAgICBTZXJ2ZXJTaWRlRW5jcnlwdGlvbkNvbmZpZ3VyYXRpb246IFtcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgU2VydmVyU2lkZUVuY3J5cHRpb25CeURlZmF1bHQ6IHtcbiAgICAgICAgICAgICAgICBTU0VBbGdvcml0aG06IFwiQUVTMjU2XCIsXG4gICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICB9LFxuICAgICAgICAgIF0sXG4gICAgICAgIH0sXG4gICAgICB9KTtcbiAgICB9KTtcblxuICAgIHRlc3QoXCJjcmVhdGVzIENsb3VkRnJvbnQgT3JpZ2luIEFjY2VzcyBJZGVudGl0eVwiLCAoKSA9PiB7XG4gICAgICBjb25zdCBwcm9wczogV2Vic2l0ZVByb3BzID0ge1xuICAgICAgICBidWNrZXROYW1lOiBcInRlc3Qtd2Vic2l0ZS1idWNrZXRcIixcbiAgICAgICAgaW5kZXhGaWxlOiBcImluZGV4Lmh0bWxcIixcbiAgICAgICAgZXJyb3JGaWxlOiBcImVycm9yLmh0bWxcIixcbiAgICAgIH07XG5cbiAgICAgIG5ldyBXZWJzaXRlKHN0YWNrLCBcIlRlc3RXZWJzaXRlXCIsIHByb3BzKTtcblxuICAgICAgY29uc3QgdGVtcGxhdGUgPSBUZW1wbGF0ZS5mcm9tU3RhY2soc3RhY2spO1xuXG4gICAgICB0ZW1wbGF0ZS5oYXNSZXNvdXJjZVByb3BlcnRpZXMoXG4gICAgICAgIFwiQVdTOjpDbG91ZEZyb250OjpDbG91ZEZyb250T3JpZ2luQWNjZXNzSWRlbnRpdHlcIixcbiAgICAgICAge1xuICAgICAgICAgIENsb3VkRnJvbnRPcmlnaW5BY2Nlc3NJZGVudGl0eUNvbmZpZzoge1xuICAgICAgICAgICAgQ29tbWVudDogTWF0Y2guYW55VmFsdWUoKSxcbiAgICAgICAgICB9LFxuICAgICAgICB9LFxuICAgICAgKTtcbiAgICB9KTtcblxuICAgIHRlc3QoXCJjcmVhdGVzIENsb3VkRnJvbnQgZGlzdHJpYnV0aW9uIHdpdGggY29ycmVjdCBjb25maWd1cmF0aW9uXCIsICgpID0+IHtcbiAgICAgIGNvbnN0IHByb3BzOiBXZWJzaXRlUHJvcHMgPSB7XG4gICAgICAgIGJ1Y2tldE5hbWU6IFwidGVzdC13ZWJzaXRlLWJ1Y2tldFwiLFxuICAgICAgICBpbmRleEZpbGU6IFwiaW5kZXguaHRtbFwiLFxuICAgICAgICBlcnJvckZpbGU6IFwiZXJyb3IuaHRtbFwiLFxuICAgICAgfTtcblxuICAgICAgbmV3IFdlYnNpdGUoc3RhY2ssIFwiVGVzdFdlYnNpdGVcIiwgcHJvcHMpO1xuXG4gICAgICBjb25zdCB0ZW1wbGF0ZSA9IFRlbXBsYXRlLmZyb21TdGFjayhzdGFjayk7XG5cbiAgICAgIHRlbXBsYXRlLmhhc1Jlc291cmNlUHJvcGVydGllcyhcIkFXUzo6Q2xvdWRGcm9udDo6RGlzdHJpYnV0aW9uXCIsIHtcbiAgICAgICAgRGlzdHJpYnV0aW9uQ29uZmlnOiB7XG4gICAgICAgICAgRGVmYXVsdENhY2hlQmVoYXZpb3I6IHtcbiAgICAgICAgICAgIFZpZXdlclByb3RvY29sUG9saWN5OiBcInJlZGlyZWN0LXRvLWh0dHBzXCIsXG4gICAgICAgICAgfSxcbiAgICAgICAgICBDdXN0b21FcnJvclJlc3BvbnNlczogW1xuICAgICAgICAgICAge1xuICAgICAgICAgICAgICBFcnJvckNvZGU6IDQwNCxcbiAgICAgICAgICAgICAgUmVzcG9uc2VDb2RlOiA0MDQsXG4gICAgICAgICAgICAgIFJlc3BvbnNlUGFnZVBhdGg6IFwiLzQwNC5odG1sXCIsXG4gICAgICAgICAgICAgIEVycm9yQ2FjaGluZ01pblRUTDogMTgwMCxcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgXSxcbiAgICAgICAgICBQcmljZUNsYXNzOiBcIlByaWNlQ2xhc3NfMTAwXCIsXG4gICAgICAgICAgRW5hYmxlZDogdHJ1ZSxcbiAgICAgICAgfSxcbiAgICAgIH0pO1xuICAgIH0pO1xuICB9KTtcblxuICBkZXNjcmliZShcIkN1c3RvbSBlcnJvciBwYWdlIGNvbmZpZ3VyYXRpb25cIiwgKCkgPT4ge1xuICAgIHRlc3QoXCJ1c2VzIGN1c3RvbSBub3QgZm91bmQgcmVzcG9uc2UgcGFnZSBwYXRoXCIsICgpID0+IHtcbiAgICAgIGNvbnN0IHByb3BzOiBXZWJzaXRlUHJvcHMgPSB7XG4gICAgICAgIGJ1Y2tldE5hbWU6IFwidGVzdC13ZWJzaXRlLWJ1Y2tldFwiLFxuICAgICAgICBpbmRleEZpbGU6IFwiaW5kZXguaHRtbFwiLFxuICAgICAgICBlcnJvckZpbGU6IFwiZXJyb3IuaHRtbFwiLFxuICAgICAgICBub3RGb3VuZFJlc3BvbnNlUGFnZVBhdGg6IFwiL2N1c3RvbS00MDQuaHRtbFwiLFxuICAgICAgfTtcblxuICAgICAgbmV3IFdlYnNpdGUoc3RhY2ssIFwiVGVzdFdlYnNpdGVcIiwgcHJvcHMpO1xuXG4gICAgICBjb25zdCB0ZW1wbGF0ZSA9IFRlbXBsYXRlLmZyb21TdGFjayhzdGFjayk7XG5cbiAgICAgIHRlbXBsYXRlLmhhc1Jlc291cmNlUHJvcGVydGllcyhcIkFXUzo6Q2xvdWRGcm9udDo6RGlzdHJpYnV0aW9uXCIsIHtcbiAgICAgICAgRGlzdHJpYnV0aW9uQ29uZmlnOiB7XG4gICAgICAgICAgQ3VzdG9tRXJyb3JSZXNwb25zZXM6IFtcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgRXJyb3JDb2RlOiA0MDQsXG4gICAgICAgICAgICAgIFJlc3BvbnNlQ29kZTogNDA0LFxuICAgICAgICAgICAgICBSZXNwb25zZVBhZ2VQYXRoOiBcIi9jdXN0b20tNDA0Lmh0bWxcIixcbiAgICAgICAgICAgICAgRXJyb3JDYWNoaW5nTWluVFRMOiAxODAwLFxuICAgICAgICAgICAgfSxcbiAgICAgICAgICBdLFxuICAgICAgICB9LFxuICAgICAgfSk7XG4gICAgfSk7XG5cbiAgICB0ZXN0KFwidXNlcyBkZWZhdWx0IDQwNC5odG1sIHdoZW4gbm90Rm91bmRSZXNwb25zZVBhZ2VQYXRoIGlzIG5vdCBwcm92aWRlZFwiLCAoKSA9PiB7XG4gICAgICBjb25zdCBwcm9wczogV2Vic2l0ZVByb3BzID0ge1xuICAgICAgICBidWNrZXROYW1lOiBcInRlc3Qtd2Vic2l0ZS1idWNrZXRcIixcbiAgICAgICAgaW5kZXhGaWxlOiBcImluZGV4Lmh0bWxcIixcbiAgICAgICAgZXJyb3JGaWxlOiBcImVycm9yLmh0bWxcIixcbiAgICAgIH07XG5cbiAgICAgIG5ldyBXZWJzaXRlKHN0YWNrLCBcIlRlc3RXZWJzaXRlXCIsIHByb3BzKTtcblxuICAgICAgY29uc3QgdGVtcGxhdGUgPSBUZW1wbGF0ZS5mcm9tU3RhY2soc3RhY2spO1xuXG4gICAgICB0ZW1wbGF0ZS5oYXNSZXNvdXJjZVByb3BlcnRpZXMoXCJBV1M6OkNsb3VkRnJvbnQ6OkRpc3RyaWJ1dGlvblwiLCB7XG4gICAgICAgIERpc3RyaWJ1dGlvbkNvbmZpZzoge1xuICAgICAgICAgIEN1c3RvbUVycm9yUmVzcG9uc2VzOiBbXG4gICAgICAgICAgICB7XG4gICAgICAgICAgICAgIEVycm9yQ29kZTogNDA0LFxuICAgICAgICAgICAgICBSZXNwb25zZUNvZGU6IDQwNCxcbiAgICAgICAgICAgICAgUmVzcG9uc2VQYWdlUGF0aDogXCIvNDA0Lmh0bWxcIixcbiAgICAgICAgICAgICAgRXJyb3JDYWNoaW5nTWluVFRMOiAxODAwLFxuICAgICAgICAgICAgfSxcbiAgICAgICAgICBdLFxuICAgICAgICB9LFxuICAgICAgfSk7XG4gICAgfSk7XG4gIH0pO1xuXG4gIGRlc2NyaWJlKFwiRG9tYWluIGNvbmZpZ3VyYXRpb25cIiwgKCkgPT4ge1xuICAgIGNvbnN0IGRvbWFpbkNvbmZpZzogRG9tYWluQ29uZmlnID0ge1xuICAgICAgZG9tYWluTmFtZTogXCJleGFtcGxlLmNvbVwiLFxuICAgICAgc3ViZG9tYWluTmFtZTogXCJ3d3dcIixcbiAgICAgIGNlcnRpZmljYXRlQXJuOlxuICAgICAgICBcImFybjphd3M6YWNtOnVzLWVhc3QtMToxMjM0NTY3ODkwMTI6Y2VydGlmaWNhdGUvMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MDEyXCIsXG4gICAgfTtcblxuICAgIHRlc3QoXCJjb25maWd1cmVzIENsb3VkRnJvbnQgZGlzdHJpYnV0aW9uIHdpdGggY3VzdG9tIGRvbWFpblwiLCAoKSA9PiB7XG4gICAgICBjb25zdCBwcm9wczogV2Vic2l0ZVByb3BzID0ge1xuICAgICAgICBidWNrZXROYW1lOiBcInRlc3Qtd2Vic2l0ZS1idWNrZXRcIixcbiAgICAgICAgaW5kZXhGaWxlOiBcImluZGV4Lmh0bWxcIixcbiAgICAgICAgZXJyb3JGaWxlOiBcImVycm9yLmh0bWxcIixcbiAgICAgICAgZG9tYWluQ29uZmlnLFxuICAgICAgfTtcblxuICAgICAgbmV3IFdlYnNpdGUoc3RhY2ssIFwiVGVzdFdlYnNpdGVcIiwgcHJvcHMpO1xuXG4gICAgICBjb25zdCB0ZW1wbGF0ZSA9IFRlbXBsYXRlLmZyb21TdGFjayhzdGFjayk7XG5cbiAgICAgIHRlbXBsYXRlLmhhc1Jlc291cmNlUHJvcGVydGllcyhcIkFXUzo6Q2xvdWRGcm9udDo6RGlzdHJpYnV0aW9uXCIsIHtcbiAgICAgICAgRGlzdHJpYnV0aW9uQ29uZmlnOiB7XG4gICAgICAgICAgQWxpYXNlczogW1wid3d3LmV4YW1wbGUuY29tXCJdLFxuICAgICAgICAgIFZpZXdlckNlcnRpZmljYXRlOiB7XG4gICAgICAgICAgICBBY21DZXJ0aWZpY2F0ZUFybjogZG9tYWluQ29uZmlnLmNlcnRpZmljYXRlQXJuLFxuICAgICAgICAgICAgU3NsU3VwcG9ydE1ldGhvZDogXCJzbmktb25seVwiLFxuICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICB9KTtcbiAgICB9KTtcblxuICAgIHRlc3QoXCJjcmVhdGVzIFJvdXRlNTMgQSByZWNvcmQgd2hlbiBkb21haW4gY29uZmlnIGlzIHByb3ZpZGVkXCIsICgpID0+IHtcbiAgICAgIGNvbnN0IHByb3BzOiBXZWJzaXRlUHJvcHMgPSB7XG4gICAgICAgIGJ1Y2tldE5hbWU6IFwidGVzdC13ZWJzaXRlLWJ1Y2tldFwiLFxuICAgICAgICBpbmRleEZpbGU6IFwiaW5kZXguaHRtbFwiLFxuICAgICAgICBlcnJvckZpbGU6IFwiZXJyb3IuaHRtbFwiLFxuICAgICAgICBkb21haW5Db25maWcsXG4gICAgICB9O1xuXG4gICAgICBuZXcgV2Vic2l0ZShzdGFjaywgXCJUZXN0V2Vic2l0ZVwiLCBwcm9wcyk7XG5cbiAgICAgIGNvbnN0IHRlbXBsYXRlID0gVGVtcGxhdGUuZnJvbVN0YWNrKHN0YWNrKTtcblxuICAgICAgdGVtcGxhdGUuaGFzUmVzb3VyY2VQcm9wZXJ0aWVzKFwiQVdTOjpSb3V0ZTUzOjpSZWNvcmRTZXRcIiwge1xuICAgICAgICBUeXBlOiBcIkFcIixcbiAgICAgICAgTmFtZTogXCJ3d3cuZXhhbXBsZS5jb20uXCIsXG4gICAgICAgIEFsaWFzVGFyZ2V0OiB7XG4gICAgICAgICAgRE5TTmFtZTogTWF0Y2guYW55VmFsdWUoKSxcbiAgICAgICAgICBIb3N0ZWRab25lSWQ6IE1hdGNoLmFueVZhbHVlKCksXG4gICAgICAgIH0sXG4gICAgICB9KTtcbiAgICB9KTtcblxuICAgIHRlc3QoXCJoYW5kbGVzIGRvbWFpbiB3aXRob3V0IHN1YmRvbWFpblwiLCAoKSA9PiB7XG4gICAgICBjb25zdCBkb21haW5Db25maWdXaXRob3V0U3ViOiBEb21haW5Db25maWcgPSB7XG4gICAgICAgIGRvbWFpbk5hbWU6IFwiZXhhbXBsZS5jb21cIixcbiAgICAgICAgc3ViZG9tYWluTmFtZTogXCJcIixcbiAgICAgICAgY2VydGlmaWNhdGVBcm46XG4gICAgICAgICAgXCJhcm46YXdzOmFjbTp1cy1lYXN0LTE6MTIzNDU2Nzg5MDEyOmNlcnRpZmljYXRlLzEyMzQ1Njc4LTEyMzQtMTIzNC0xMjM0LTEyMzQ1Njc4OTAxMlwiLFxuICAgICAgfTtcblxuICAgICAgY29uc3QgcHJvcHM6IFdlYnNpdGVQcm9wcyA9IHtcbiAgICAgICAgYnVja2V0TmFtZTogXCJ0ZXN0LXdlYnNpdGUtYnVja2V0XCIsXG4gICAgICAgIGluZGV4RmlsZTogXCJpbmRleC5odG1sXCIsXG4gICAgICAgIGVycm9yRmlsZTogXCJlcnJvci5odG1sXCIsXG4gICAgICAgIGRvbWFpbkNvbmZpZzogZG9tYWluQ29uZmlnV2l0aG91dFN1YixcbiAgICAgIH07XG5cbiAgICAgIG5ldyBXZWJzaXRlKHN0YWNrLCBcIlRlc3RXZWJzaXRlXCIsIHByb3BzKTtcblxuICAgICAgY29uc3QgdGVtcGxhdGUgPSBUZW1wbGF0ZS5mcm9tU3RhY2soc3RhY2spO1xuXG4gICAgICB0ZW1wbGF0ZS5oYXNSZXNvdXJjZVByb3BlcnRpZXMoXCJBV1M6OkNsb3VkRnJvbnQ6OkRpc3RyaWJ1dGlvblwiLCB7XG4gICAgICAgIERpc3RyaWJ1dGlvbkNvbmZpZzoge1xuICAgICAgICAgIEFsaWFzZXM6IFtcImV4YW1wbGUuY29tXCJdLFxuICAgICAgICB9LFxuICAgICAgfSk7XG5cbiAgICAgIHRlbXBsYXRlLmhhc1Jlc291cmNlUHJvcGVydGllcyhcIkFXUzo6Um91dGU1Mzo6UmVjb3JkU2V0XCIsIHtcbiAgICAgICAgVHlwZTogXCJBXCIsXG4gICAgICAgIE5hbWU6IFwiZXhhbXBsZS5jb20uXCIsXG4gICAgICB9KTtcbiAgICB9KTtcbiAgfSk7XG5cbiAgZGVzY3JpYmUoXCJQcml2YXRlIG1ldGhvZHNcIiwgKCkgPT4ge1xuICAgIHRlc3QoXCJfZ2V0RnVsbERvbWFpbk5hbWUgcmV0dXJucyBjb3JyZWN0IGRvbWFpbiB3aXRoIHN1YmRvbWFpblwiLCAoKSA9PiB7XG4gICAgICBjb25zdCBwcm9wczogV2Vic2l0ZVByb3BzID0ge1xuICAgICAgICBidWNrZXROYW1lOiBcInRlc3QtYnVja2V0XCIsXG4gICAgICAgIGluZGV4RmlsZTogXCJpbmRleC5odG1sXCIsXG4gICAgICAgIGVycm9yRmlsZTogXCJlcnJvci5odG1sXCIsXG4gICAgICB9O1xuXG4gICAgICBjb25zdCBjb25zdHJ1Y3QgPSBuZXcgV2Vic2l0ZShzdGFjaywgXCJUZXN0V2Vic2l0ZVwiLCBwcm9wcyk7XG5cbiAgICAgIC8vIEFjY2VzcyBwcml2YXRlIG1ldGhvZCB0aHJvdWdoIGJyYWNrZXQgbm90YXRpb24gZm9yIHRlc3RpbmdcbiAgICAgIGNvbnN0IGdldEZ1bGxEb21haW5OYW1lID0gKGNvbnN0cnVjdCBhcyBhbnkpLl9nZXRGdWxsRG9tYWluTmFtZTtcblxuICAgICAgY29uc3QgZG9tYWluQ29uZmlnOiBEb21haW5Db25maWcgPSB7XG4gICAgICAgIGRvbWFpbk5hbWU6IFwiZXhhbXBsZS5jb21cIixcbiAgICAgICAgc3ViZG9tYWluTmFtZTogXCJibG9nXCIsXG4gICAgICAgIGNlcnRpZmljYXRlQXJuOiBcImFybjp0ZXN0XCIsXG4gICAgICB9O1xuXG4gICAgICBjb25zdCByZXN1bHQgPSBnZXRGdWxsRG9tYWluTmFtZShkb21haW5Db25maWcpO1xuICAgICAgZXhwZWN0KHJlc3VsdCkudG9CZShcImJsb2cuZXhhbXBsZS5jb21cIik7XG4gICAgfSk7XG5cbiAgICB0ZXN0KFwiX2dldEZ1bGxEb21haW5OYW1lIHJldHVybnMgcm9vdCBkb21haW4gd2hlbiBzdWJkb21haW4gaXMgZW1wdHlcIiwgKCkgPT4ge1xuICAgICAgY29uc3QgcHJvcHM6IFdlYnNpdGVQcm9wcyA9IHtcbiAgICAgICAgYnVja2V0TmFtZTogXCJ0ZXN0LWJ1Y2tldFwiLFxuICAgICAgICBpbmRleEZpbGU6IFwiaW5kZXguaHRtbFwiLFxuICAgICAgICBlcnJvckZpbGU6IFwiZXJyb3IuaHRtbFwiLFxuICAgICAgfTtcblxuICAgICAgY29uc3QgY29uc3RydWN0ID0gbmV3IFdlYnNpdGUoc3RhY2ssIFwiVGVzdFdlYnNpdGVcIiwgcHJvcHMpO1xuXG4gICAgICBjb25zdCBnZXRGdWxsRG9tYWluTmFtZSA9IChjb25zdHJ1Y3QgYXMgYW55KS5fZ2V0RnVsbERvbWFpbk5hbWU7XG5cbiAgICAgIGNvbnN0IGRvbWFpbkNvbmZpZzogRG9tYWluQ29uZmlnID0ge1xuICAgICAgICBkb21haW5OYW1lOiBcImV4YW1wbGUuY29tXCIsXG4gICAgICAgIHN1YmRvbWFpbk5hbWU6IFwiXCIsXG4gICAgICAgIGNlcnRpZmljYXRlQXJuOiBcImFybjp0ZXN0XCIsXG4gICAgICB9O1xuXG4gICAgICBjb25zdCByZXN1bHQgPSBnZXRGdWxsRG9tYWluTmFtZShkb21haW5Db25maWcpO1xuICAgICAgZXhwZWN0KHJlc3VsdCkudG9CZShcImV4YW1wbGUuY29tXCIpO1xuICAgIH0pO1xuICB9KTtcblxuICBkZXNjcmliZShcIkVkZ2UgY2FzZXNcIiwgKCkgPT4ge1xuICAgIHRlc3QoXCJoYW5kbGVzIG1pbmltYWwgY29uZmlndXJhdGlvblwiLCAoKSA9PiB7XG4gICAgICBjb25zdCBwcm9wczogV2Vic2l0ZVByb3BzID0ge1xuICAgICAgICBidWNrZXROYW1lOiBcIm1pbmltYWwtYnVja2V0XCIsXG4gICAgICAgIGluZGV4RmlsZTogXCJpbmRleC5odG1sXCIsXG4gICAgICAgIGVycm9yRmlsZTogXCJlcnJvci5odG1sXCIsXG4gICAgICB9O1xuXG4gICAgICBleHBlY3QoKCkgPT4ge1xuICAgICAgICBuZXcgV2Vic2l0ZShzdGFjaywgXCJUZXN0V2Vic2l0ZVwiLCBwcm9wcyk7XG4gICAgICB9KS5ub3QudG9UaHJvdygpO1xuXG4gICAgICBjb25zdCB0ZW1wbGF0ZSA9IFRlbXBsYXRlLmZyb21TdGFjayhzdGFjayk7XG4gICAgICB0ZW1wbGF0ZS5yZXNvdXJjZUNvdW50SXMoXCJBV1M6OlMzOjpCdWNrZXRcIiwgMSk7XG4gICAgICB0ZW1wbGF0ZS5yZXNvdXJjZUNvdW50SXMoXCJBV1M6OkNsb3VkRnJvbnQ6OkRpc3RyaWJ1dGlvblwiLCAxKTtcbiAgICAgIHRlbXBsYXRlLnJlc291cmNlQ291bnRJcyhcbiAgICAgICAgXCJBV1M6OkNsb3VkRnJvbnQ6OkNsb3VkRnJvbnRPcmlnaW5BY2Nlc3NJZGVudGl0eVwiLFxuICAgICAgICAxLFxuICAgICAgKTtcbiAgICB9KTtcblxuICAgIHRlc3QoXCJkb2VzIG5vdCBjcmVhdGUgUm91dGU1MyByZXNvdXJjZXMgd2hlbiBkb21haW4gY29uZmlnIGlzIG5vdCBwcm92aWRlZFwiLCAoKSA9PiB7XG4gICAgICBjb25zdCBwcm9wczogV2Vic2l0ZVByb3BzID0ge1xuICAgICAgICBidWNrZXROYW1lOiBcInRlc3QtYnVja2V0XCIsXG4gICAgICAgIGluZGV4RmlsZTogXCJpbmRleC5odG1sXCIsXG4gICAgICAgIGVycm9yRmlsZTogXCJlcnJvci5odG1sXCIsXG4gICAgICB9O1xuXG4gICAgICBuZXcgV2Vic2l0ZShzdGFjaywgXCJUZXN0V2Vic2l0ZVwiLCBwcm9wcyk7XG5cbiAgICAgIGNvbnN0IHRlbXBsYXRlID0gVGVtcGxhdGUuZnJvbVN0YWNrKHN0YWNrKTtcbiAgICAgIHRlbXBsYXRlLnJlc291cmNlQ291bnRJcyhcIkFXUzo6Um91dGU1Mzo6UmVjb3JkU2V0XCIsIDApO1xuICAgIH0pO1xuICB9KTtcbn0pO1xuIl19
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { Template, Match } from "aws-cdk-lib/assertions";
|
|
2
|
+
import * as cdk from "aws-cdk-lib";
|
|
3
|
+
import { Website, WebsiteProps, DomainConfig } from "../lib";
|
|
4
|
+
|
|
5
|
+
describe("Website", () => {
|
|
6
|
+
let app: cdk.App;
|
|
7
|
+
let stack: cdk.Stack;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
app = new cdk.App();
|
|
11
|
+
stack = new cdk.Stack(app, "TestStack", {
|
|
12
|
+
env: { account: "123456789012", region: "us-east-1" },
|
|
13
|
+
});
|
|
14
|
+
stack.node.setContext(
|
|
15
|
+
"hosted-zone:account=123456789012:domainName=example.com:region=us-east-1",
|
|
16
|
+
{
|
|
17
|
+
Id: "/hostedzone/Z123456789012",
|
|
18
|
+
Name: "example.com.",
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("Basic functionality", () => {
|
|
24
|
+
test("creates S3 bucket with basic configuration", () => {
|
|
25
|
+
const props: WebsiteProps = {
|
|
26
|
+
bucketName: "test-website-bucket",
|
|
27
|
+
indexFile: "index.html",
|
|
28
|
+
errorFile: "error.html",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
new Website(stack, "TestWebsite", props);
|
|
32
|
+
|
|
33
|
+
const template = Template.fromStack(stack);
|
|
34
|
+
|
|
35
|
+
template.hasResourceProperties("AWS::S3::Bucket", {
|
|
36
|
+
WebsiteConfiguration: {
|
|
37
|
+
IndexDocument: "index.html",
|
|
38
|
+
ErrorDocument: "error.html",
|
|
39
|
+
},
|
|
40
|
+
BucketEncryption: {
|
|
41
|
+
ServerSideEncryptionConfiguration: [
|
|
42
|
+
{
|
|
43
|
+
ServerSideEncryptionByDefault: {
|
|
44
|
+
SSEAlgorithm: "AES256",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("creates CloudFront Origin Access Identity", () => {
|
|
53
|
+
const props: WebsiteProps = {
|
|
54
|
+
bucketName: "test-website-bucket",
|
|
55
|
+
indexFile: "index.html",
|
|
56
|
+
errorFile: "error.html",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
new Website(stack, "TestWebsite", props);
|
|
60
|
+
|
|
61
|
+
const template = Template.fromStack(stack);
|
|
62
|
+
|
|
63
|
+
template.hasResourceProperties(
|
|
64
|
+
"AWS::CloudFront::CloudFrontOriginAccessIdentity",
|
|
65
|
+
{
|
|
66
|
+
CloudFrontOriginAccessIdentityConfig: {
|
|
67
|
+
Comment: Match.anyValue(),
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("creates CloudFront distribution with correct configuration", () => {
|
|
74
|
+
const props: WebsiteProps = {
|
|
75
|
+
bucketName: "test-website-bucket",
|
|
76
|
+
indexFile: "index.html",
|
|
77
|
+
errorFile: "error.html",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
new Website(stack, "TestWebsite", props);
|
|
81
|
+
|
|
82
|
+
const template = Template.fromStack(stack);
|
|
83
|
+
|
|
84
|
+
template.hasResourceProperties("AWS::CloudFront::Distribution", {
|
|
85
|
+
DistributionConfig: {
|
|
86
|
+
DefaultCacheBehavior: {
|
|
87
|
+
ViewerProtocolPolicy: "redirect-to-https",
|
|
88
|
+
},
|
|
89
|
+
CustomErrorResponses: [
|
|
90
|
+
{
|
|
91
|
+
ErrorCode: 404,
|
|
92
|
+
ResponseCode: 404,
|
|
93
|
+
ResponsePagePath: "/404.html",
|
|
94
|
+
ErrorCachingMinTTL: 1800,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
PriceClass: "PriceClass_100",
|
|
98
|
+
Enabled: true,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("Custom error page configuration", () => {
|
|
105
|
+
test("uses custom not found response page path", () => {
|
|
106
|
+
const props: WebsiteProps = {
|
|
107
|
+
bucketName: "test-website-bucket",
|
|
108
|
+
indexFile: "index.html",
|
|
109
|
+
errorFile: "error.html",
|
|
110
|
+
notFoundResponsePagePath: "/custom-404.html",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
new Website(stack, "TestWebsite", props);
|
|
114
|
+
|
|
115
|
+
const template = Template.fromStack(stack);
|
|
116
|
+
|
|
117
|
+
template.hasResourceProperties("AWS::CloudFront::Distribution", {
|
|
118
|
+
DistributionConfig: {
|
|
119
|
+
CustomErrorResponses: [
|
|
120
|
+
{
|
|
121
|
+
ErrorCode: 404,
|
|
122
|
+
ResponseCode: 404,
|
|
123
|
+
ResponsePagePath: "/custom-404.html",
|
|
124
|
+
ErrorCachingMinTTL: 1800,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("uses default 404.html when notFoundResponsePagePath is not provided", () => {
|
|
132
|
+
const props: WebsiteProps = {
|
|
133
|
+
bucketName: "test-website-bucket",
|
|
134
|
+
indexFile: "index.html",
|
|
135
|
+
errorFile: "error.html",
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
new Website(stack, "TestWebsite", props);
|
|
139
|
+
|
|
140
|
+
const template = Template.fromStack(stack);
|
|
141
|
+
|
|
142
|
+
template.hasResourceProperties("AWS::CloudFront::Distribution", {
|
|
143
|
+
DistributionConfig: {
|
|
144
|
+
CustomErrorResponses: [
|
|
145
|
+
{
|
|
146
|
+
ErrorCode: 404,
|
|
147
|
+
ResponseCode: 404,
|
|
148
|
+
ResponsePagePath: "/404.html",
|
|
149
|
+
ErrorCachingMinTTL: 1800,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("Domain configuration", () => {
|
|
158
|
+
const domainConfig: DomainConfig = {
|
|
159
|
+
domainName: "example.com",
|
|
160
|
+
subdomainName: "www",
|
|
161
|
+
certificateArn:
|
|
162
|
+
"arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
test("configures CloudFront distribution with custom domain", () => {
|
|
166
|
+
const props: WebsiteProps = {
|
|
167
|
+
bucketName: "test-website-bucket",
|
|
168
|
+
indexFile: "index.html",
|
|
169
|
+
errorFile: "error.html",
|
|
170
|
+
domainConfig,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
new Website(stack, "TestWebsite", props);
|
|
174
|
+
|
|
175
|
+
const template = Template.fromStack(stack);
|
|
176
|
+
|
|
177
|
+
template.hasResourceProperties("AWS::CloudFront::Distribution", {
|
|
178
|
+
DistributionConfig: {
|
|
179
|
+
Aliases: ["www.example.com"],
|
|
180
|
+
ViewerCertificate: {
|
|
181
|
+
AcmCertificateArn: domainConfig.certificateArn,
|
|
182
|
+
SslSupportMethod: "sni-only",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("creates Route53 A record when domain config is provided", () => {
|
|
189
|
+
const props: WebsiteProps = {
|
|
190
|
+
bucketName: "test-website-bucket",
|
|
191
|
+
indexFile: "index.html",
|
|
192
|
+
errorFile: "error.html",
|
|
193
|
+
domainConfig,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
new Website(stack, "TestWebsite", props);
|
|
197
|
+
|
|
198
|
+
const template = Template.fromStack(stack);
|
|
199
|
+
|
|
200
|
+
template.hasResourceProperties("AWS::Route53::RecordSet", {
|
|
201
|
+
Type: "A",
|
|
202
|
+
Name: "www.example.com.",
|
|
203
|
+
AliasTarget: {
|
|
204
|
+
DNSName: Match.anyValue(),
|
|
205
|
+
HostedZoneId: Match.anyValue(),
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("handles domain without subdomain", () => {
|
|
211
|
+
const domainConfigWithoutSub: DomainConfig = {
|
|
212
|
+
domainName: "example.com",
|
|
213
|
+
subdomainName: "",
|
|
214
|
+
certificateArn:
|
|
215
|
+
"arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const props: WebsiteProps = {
|
|
219
|
+
bucketName: "test-website-bucket",
|
|
220
|
+
indexFile: "index.html",
|
|
221
|
+
errorFile: "error.html",
|
|
222
|
+
domainConfig: domainConfigWithoutSub,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
new Website(stack, "TestWebsite", props);
|
|
226
|
+
|
|
227
|
+
const template = Template.fromStack(stack);
|
|
228
|
+
|
|
229
|
+
template.hasResourceProperties("AWS::CloudFront::Distribution", {
|
|
230
|
+
DistributionConfig: {
|
|
231
|
+
Aliases: ["example.com"],
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
template.hasResourceProperties("AWS::Route53::RecordSet", {
|
|
236
|
+
Type: "A",
|
|
237
|
+
Name: "example.com.",
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("Private methods", () => {
|
|
243
|
+
test("_getFullDomainName returns correct domain with subdomain", () => {
|
|
244
|
+
const props: WebsiteProps = {
|
|
245
|
+
bucketName: "test-bucket",
|
|
246
|
+
indexFile: "index.html",
|
|
247
|
+
errorFile: "error.html",
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const construct = new Website(stack, "TestWebsite", props);
|
|
251
|
+
|
|
252
|
+
// Access private method through bracket notation for testing
|
|
253
|
+
const getFullDomainName = (construct as any)._getFullDomainName;
|
|
254
|
+
|
|
255
|
+
const domainConfig: DomainConfig = {
|
|
256
|
+
domainName: "example.com",
|
|
257
|
+
subdomainName: "blog",
|
|
258
|
+
certificateArn: "arn:test",
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const result = getFullDomainName(domainConfig);
|
|
262
|
+
expect(result).toBe("blog.example.com");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("_getFullDomainName returns root domain when subdomain is empty", () => {
|
|
266
|
+
const props: WebsiteProps = {
|
|
267
|
+
bucketName: "test-bucket",
|
|
268
|
+
indexFile: "index.html",
|
|
269
|
+
errorFile: "error.html",
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const construct = new Website(stack, "TestWebsite", props);
|
|
273
|
+
|
|
274
|
+
const getFullDomainName = (construct as any)._getFullDomainName;
|
|
275
|
+
|
|
276
|
+
const domainConfig: DomainConfig = {
|
|
277
|
+
domainName: "example.com",
|
|
278
|
+
subdomainName: "",
|
|
279
|
+
certificateArn: "arn:test",
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const result = getFullDomainName(domainConfig);
|
|
283
|
+
expect(result).toBe("example.com");
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("Edge cases", () => {
|
|
288
|
+
test("handles minimal configuration", () => {
|
|
289
|
+
const props: WebsiteProps = {
|
|
290
|
+
bucketName: "minimal-bucket",
|
|
291
|
+
indexFile: "index.html",
|
|
292
|
+
errorFile: "error.html",
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
expect(() => {
|
|
296
|
+
new Website(stack, "TestWebsite", props);
|
|
297
|
+
}).not.toThrow();
|
|
298
|
+
|
|
299
|
+
const template = Template.fromStack(stack);
|
|
300
|
+
template.resourceCountIs("AWS::S3::Bucket", 1);
|
|
301
|
+
template.resourceCountIs("AWS::CloudFront::Distribution", 1);
|
|
302
|
+
template.resourceCountIs(
|
|
303
|
+
"AWS::CloudFront::CloudFrontOriginAccessIdentity",
|
|
304
|
+
1,
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("does not create Route53 resources when domain config is not provided", () => {
|
|
309
|
+
const props: WebsiteProps = {
|
|
310
|
+
bucketName: "test-bucket",
|
|
311
|
+
indexFile: "index.html",
|
|
312
|
+
errorFile: "error.html",
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
new Website(stack, "TestWebsite", props);
|
|
316
|
+
|
|
317
|
+
const template = Template.fromStack(stack);
|
|
318
|
+
template.resourceCountIs("AWS::Route53::RecordSet", 0);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
});
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
name: Run Tests
|
|
2
|
-
on:
|
|
3
|
-
push:
|
|
4
|
-
pull_request:
|
|
5
|
-
jobs:
|
|
6
|
-
test:
|
|
7
|
-
runs-on: ubuntu-latest
|
|
8
|
-
steps:
|
|
9
|
-
- name: Checkout repository
|
|
10
|
-
uses: actions/checkout@v4
|
|
11
|
-
|
|
12
|
-
- name: Set up Node.js
|
|
13
|
-
uses: actions/setup-node@v4
|
|
14
|
-
with:
|
|
15
|
-
node-version: "20"
|
|
16
|
-
cache: "npm"
|
|
17
|
-
|
|
18
|
-
- name: Install dependencies
|
|
19
|
-
run: npm ci
|
|
20
|
-
|
|
21
|
-
- name: Run Jest tests
|
|
22
|
-
env:
|
|
23
|
-
CI: true
|
|
24
|
-
run: npm test
|