Skip to content
Merged
1 change: 1 addition & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
TEST_USER_ID: ${{ secrets.TEST_USER_ID }}
TEST_USER_REFRESH_TOKEN: ${{ secrets.TEST_USER_REFRESH_TOKEN }}
AUTH_REDIRECT_URI: 'https://test/callback/'
CREATE_PLAYLIST_LAMBDA_ARN: 'arn:test'

- name: Upload build artifacts
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/cypress-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ jobs:
SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }}
TEST_USER_REFRESH_TOKEN: ${{ secrets.TEST_USER_REFRESH_TOKEN }}
AUTH_REDIRECT_URI: 'https://${{ inputs.domain }}/callback.html'
CREATE_PLAYLIST_LAMBDA_ARN: 'arn:test'
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
TF_VAR_spotify_client_id: ${{ secrets.SPOTIFY_CLIENT_ID }}
TF_VAR_spotify_client_secret: ${{ secrets.SPOTIFY_CLIENT_SECRET }}
TF_VAR_auth_redirect_uri: 'https://d1e7htx1c4j3w0.cloudfront.net/callback.html'
TF_VAR_create_playlist_lambda_arn: 'arn:aws:lambda:us-east-1:765212935426:function:all-tracks-create-playlist-processor'

- name: Terraform Apply
run: terraform apply -input=false -auto-approve
Expand All @@ -57,6 +58,7 @@ jobs:
TF_VAR_spotify_client_id: ${{ secrets.SPOTIFY_CLIENT_ID }}
TF_VAR_spotify_client_secret: ${{ secrets.SPOTIFY_CLIENT_SECRET }}
TF_VAR_auth_redirect_uri: 'https://d1e7htx1c4j3w0.cloudfront.net/callback.html'
TF_VAR_create_playlist_lambda_arn: 'arn:aws:lambda:us-east-1:765212935426:function:all-tracks-create-playlist-processor'

- name: Get Terraform outputs
id: tf-outputs
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/large-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ jobs:
TEST_USER_ID: ${{ secrets.TEST_USER_ID }}
TEST_USER_REFRESH_TOKEN: ${{ secrets.TEST_USER_REFRESH_TOKEN }}
AUTH_REDIRECT_URI: 'https://test/callback/'
CREATE_PLAYLIST_LAMBDA_ARN: 'arn:aws:lambda:us-east-1:765212935426:function:all-tracks-create-playlist-processor'
35 changes: 16 additions & 19 deletions cypress/e2e/index.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ describe('index', () => {
process.env.SPOTIFY_CLIENT_ID = Cypress.env('SPOTIFY_CLIENT_ID');
process.env.SPOTIFY_CLIENT_SECRET = Cypress.env('SPOTIFY_CLIENT_SECRET');
process.env.AUTH_REDIRECT_URI = Cypress.env('AUTH_REDIRECT_URI');
process.env.CREATE_PLAYLIST_LAMBDA_ARN = Cypress.env(
'CREATE_PLAYLIST_LAMBDA_ARN'
);
const accessToken = await refreshToken(
Cypress.env('TEST_USER_REFRESH_TOKEN')
);
Expand All @@ -70,6 +73,9 @@ describe('index', () => {
process.env.SPOTIFY_CLIENT_ID = Cypress.env('SPOTIFY_CLIENT_ID');
process.env.SPOTIFY_CLIENT_SECRET = Cypress.env('SPOTIFY_CLIENT_SECRET');
process.env.AUTH_REDIRECT_URI = Cypress.env('AUTH_REDIRECT_URI');
process.env.CREATE_PLAYLIST_LAMBDA_ARN = Cypress.env(
'CREATE_PLAYLIST_LAMBDA_ARN'
);
const accessToken = await refreshToken(
Cypress.env('TEST_USER_REFRESH_TOKEN')
);
Expand All @@ -84,11 +90,14 @@ describe('index', () => {
).should('not.be.visible');
});

it('creates a playlist when the create playlist button is clicked', () => {
it("informs user playlist will be available soon when 'Create Playlist' is clicked", () => {
cy.wrap(null).then(async () => {
process.env.SPOTIFY_CLIENT_ID = Cypress.env('SPOTIFY_CLIENT_ID');
process.env.SPOTIFY_CLIENT_SECRET = Cypress.env('SPOTIFY_CLIENT_SECRET');
process.env.AUTH_REDIRECT_URI = Cypress.env('AUTH_REDIRECT_URI');
process.env.CREATE_PLAYLIST_LAMBDA_ARN = Cypress.env(
'CREATE_PLAYLIST_LAMBDA_ARN'
);
const accessToken = await refreshToken(
Cypress.env('TEST_USER_REFRESH_TOKEN')
);
Expand Down Expand Up @@ -122,25 +131,13 @@ describe('index', () => {
// Wait for the request to complete
cy.wait('@createPlaylistRequest', { timeout: 20000 }).then(
(interception) => {
// Verify the response contains the expected data
expect(interception.response.statusCode).to.equal(201);
expect(interception.response.body).to.have.property(
'name',
'The Beatles - All tracks'
);
expect(interception.response.body).to.have.property(
'description',
'Playlist created by all-tracks: https://d1e7htx1c4j3w0.cloudfront.net'
);
expect(interception.response.body).to.have.property('url');
// Verify the response is 200
expect(interception.response.statusCode).to.equal(200);

// Verify the result is displayed on the page
cy.contains('Playlist created successfully!').should('be.visible');
cy.contains(interception.response.body.name).should('be.visible');
cy.contains(interception.response.body.description).should(
'be.visible'
);
cy.contains(interception.response.body.url).should('be.visible');
// Verify the result message is displayed on the page
cy.contains(
'Your playlist will be available in your Spotify account in a few minutes!'
).should('be.visible');
}
);
});
Expand Down
17 changes: 17 additions & 0 deletions deploy/iam.tf
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ resource "aws_iam_role_policy_attachment" "lambda_basic" {
role = aws_iam_role.lambda_role.name
}

# IAM policy to allow API Lambda to invoke Create Playlist Lambda
resource "aws_iam_role_policy" "lambda_invoke_create_playlist" {
name = "${local.app_name}-invoke-create-playlist-policy"
role = aws_iam_role.lambda_role.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "lambda:InvokeFunction"
Effect = "Allow"
Resource = var.create_playlist_lambda_arn
}
]
})
}


#
# Create Playlist Processor Lambda IAM Resources
Expand Down
16 changes: 9 additions & 7 deletions deploy/lambda.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ resource "aws_lambda_function" "api_lambda" {

environment {
variables = {
SPOTIFY_CLIENT_ID = var.spotify_client_id
SPOTIFY_CLIENT_SECRET = var.spotify_client_secret
AUTH_REDIRECT_URI = var.auth_redirect_uri
SPOTIFY_CLIENT_ID = var.spotify_client_id
SPOTIFY_CLIENT_SECRET = var.spotify_client_secret
AUTH_REDIRECT_URI = var.auth_redirect_uri
CREATE_PLAYLIST_LAMBDA_ARN = var.create_playlist_lambda_arn
}
}

Expand Down Expand Up @@ -43,15 +44,16 @@ resource "aws_lambda_function" "create_playlist_processor_lambda" {
role = aws_iam_role.create_playlist_processor_role.arn
handler = "index.handleCreatePlaylistRequest"
runtime = "nodejs18.x"
timeout = 30
timeout = 600
source_code_hash = data.archive_file.create_playlist_processor_lambda_zip.output_base64sha256
memory_size = 1024

environment {
variables = {
SPOTIFY_CLIENT_ID = var.spotify_client_id
SPOTIFY_CLIENT_SECRET = var.spotify_client_secret
AUTH_REDIRECT_URI = var.auth_redirect_uri
SPOTIFY_CLIENT_ID = var.spotify_client_id
SPOTIFY_CLIENT_SECRET = var.spotify_client_secret
AUTH_REDIRECT_URI = var.auth_redirect_uri
CREATE_PLAYLIST_LAMBDA_ARN = var.create_playlist_lambda_arn
}
}

Expand Down
4 changes: 4 additions & 0 deletions deploy/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ variable "spotify_client_secret" {
variable "auth_redirect_uri" {
type = string
}

variable "create_playlist_lambda_arn" {
type = string
}
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ module.exports = {
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).ts'],
setupFiles: ['dotenv/config'],
testTimeout: 20000
testTimeout: 30000
};
8 changes: 4 additions & 4 deletions src/apiRequestHandlers/handleCreatePlaylistRequest.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { generateResult, ApiEvent } from '../handleApiRequest';
import { APIGatewayProxyResult } from 'aws-lambda';
import { getAccessTokenFromEvent } from './getAccessTokenFromEvent';
import { createPlaylist } from '../spotify/createPlaylist';
import { invokeCreatePlaylistLambda } from '../invokeCreatePlaylistLambda';

export const handleCreatePlaylistRequest = async (
event: ApiEvent
Expand All @@ -22,10 +22,10 @@ export const handleCreatePlaylistRequest = async (
});
}

const result = await createPlaylist({ accessToken, artistId });
await invokeCreatePlaylistLambda({ accessToken, artistId });

return generateResult({
statusCode: 201,
bodyObject: result
statusCode: 200,
bodyObject: { message: 'Playlist creation accepted' }
});
};
18 changes: 16 additions & 2 deletions src/config/getEnvVariables.small.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,42 @@ describe('getEnvVariables', () => {
const expectedClientId = 'client_id';
const expectedClientSecret = 'secret';
const expectedRedirectUri = 'redirect_uri';
const expectedLambdaArn = 'lambda_arn';

beforeEach(() => {
process.env.SPOTIFY_CLIENT_ID = expectedClientId;
process.env.SPOTIFY_CLIENT_SECRET = expectedClientSecret;
process.env.AUTH_REDIRECT_URI = expectedRedirectUri;
process.env.CREATE_PLAYLIST_LAMBDA_ARN = expectedLambdaArn;
});

it('gets env variables', async () => {
const { spotifyClientId, spotifyClientSecret, authRedirectUri } =
const { spotifyClientId, spotifyClientSecret, authRedirectUri, createPlaylistLambdaArn } =
getEnvVariables();

expect(spotifyClientId).toBe(expectedClientId);
expect(spotifyClientSecret).toBe(expectedClientSecret);
expect(authRedirectUri).toBe(expectedRedirectUri);
expect(createPlaylistLambdaArn).toBe(expectedLambdaArn);
});

it('gets different env variables', async () => {
const differentClientId = 'different_client_id';
const differentClientSecret = 'different_secret';
const differentRedirectUri = 'different_redirect_uri';
const differentLambdaArn = 'different_lambda_arn';
process.env.SPOTIFY_CLIENT_ID = differentClientId;
process.env.SPOTIFY_CLIENT_SECRET = differentClientSecret;
process.env.AUTH_REDIRECT_URI = differentRedirectUri;
process.env.CREATE_PLAYLIST_LAMBDA_ARN = differentLambdaArn;

const { spotifyClientId, spotifyClientSecret, authRedirectUri } =
const { spotifyClientId, spotifyClientSecret, authRedirectUri, createPlaylistLambdaArn } =
getEnvVariables();

expect(spotifyClientId).toBe(differentClientId);
expect(spotifyClientSecret).toBe(differentClientSecret);
expect(authRedirectUri).toBe(differentRedirectUri);
expect(createPlaylistLambdaArn).toBe(differentLambdaArn);
});

it('throws error if SPOTIFY_CLIENT_ID not set', async () => {
Expand All @@ -59,4 +65,12 @@ describe('getEnvVariables', () => {
'AUTH_REDIRECT_URI environment variable is not set'
);
});

it('throws error if CREATE_PLAYLIST_LAMBDA_ARN not set', async () => {
delete process.env.CREATE_PLAYLIST_LAMBDA_ARN;

expect(getEnvVariables).toThrow(
'CREATE_PLAYLIST_LAMBDA_ARN environment variable is not set'
);
});
});
8 changes: 7 additions & 1 deletion src/config/getEnvVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,21 @@ export const getEnvVariables = (): EnvVariables => {
throw new Error('AUTH_REDIRECT_URI environment variable is not set');
}

if (process.env.CREATE_PLAYLIST_LAMBDA_ARN === undefined) {
throw new Error('CREATE_PLAYLIST_LAMBDA_ARN environment variable is not set');
}

return {
spotifyClientId: process.env.SPOTIFY_CLIENT_ID,
spotifyClientSecret: process.env.SPOTIFY_CLIENT_SECRET,
authRedirectUri: process.env.AUTH_REDIRECT_URI
authRedirectUri: process.env.AUTH_REDIRECT_URI,
createPlaylistLambdaArn: process.env.CREATE_PLAYLIST_LAMBDA_ARN
};
};

type EnvVariables = {
spotifyClientId: string;
spotifyClientSecret: string;
authRedirectUri: string;
createPlaylistLambdaArn: string;
};
35 changes: 10 additions & 25 deletions src/handleApiRequest.small.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { getMe } from './spotify/getMe';
import { getAccessToken } from './spotify/auth/getAccessToken';
import { when } from 'jest-when';
import { getArtistsTracks } from './spotify/getArtistsTracks';
import { createPlaylist } from './spotify/createPlaylist';
import { invokeCreatePlaylistLambda } from './invokeCreatePlaylistLambda';

jest.mock('./spotify/getArtistName');
jest.mock('./spotify/getArtistsAlbums');
jest.mock('./spotify/getMe');
jest.mock('./spotify/auth/getAccessToken');
jest.mock('./spotify/getArtistsTracks');
jest.mock('./spotify/createPlaylist');
jest.mock('./invokeCreatePlaylistLambda');

const accessToken = 'accessToken';
const authorizationHeader = `Bearer ${accessToken}`;
Expand Down Expand Up @@ -456,25 +456,8 @@ describe('apiHandler', () => {
});
});

it('creates a playlist and returns the url to it', async () => {
const name = 'playlistName';
const description = 'playlistDescription';
const url = 'playlistUrl';
const id = 'playlistId';
it('invokes create playlist lambda and returns acceptance response', async () => {
const artistId = 'artistId';

when(createPlaylist)
.calledWith({
accessToken,
artistId
})
.mockResolvedValue({
id,
name,
description,
url
});

const mockEvent: ApiEvent = {
queryStringParameters: {
artistId
Expand All @@ -484,19 +467,21 @@ describe('apiHandler', () => {
Authorization: authorizationHeader
}
};

const response = await handleApiRequest(mockEvent);

expect(invokeCreatePlaylistLambda).toHaveBeenCalledWith({
accessToken,
artistId
});
expect(response).toStrictEqual({
statusCode: 201,
statusCode: 200,
headers: {
'Content-Type': 'application/json'
},
multiValueHeaders: {},
body: JSON.stringify({
id,
name,
description,
url
message: 'Playlist creation accepted'
})
});
});
Expand Down
22 changes: 22 additions & 0 deletions src/invokeCreatePlaylistLambda.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
import { getEnvVariables } from './config/getEnvVariables';

const lambdaClient = new LambdaClient({});

export const invokeCreatePlaylistLambda = async ({
accessToken,
artistId
}: {
accessToken: string;
artistId: string;
}) => {
const { createPlaylistLambdaArn } = getEnvVariables();

const invokeCommand = new InvokeCommand({
FunctionName: createPlaylistLambdaArn,
InvocationType: 'Event',
Payload: JSON.stringify({ accessToken, artistId })
});

await lambdaClient.send(invokeCommand);
};
1 change: 1 addition & 0 deletions src/spotify/auth/getAccessToken.small.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('getAccessToken', () => {
process.env.SPOTIFY_CLIENT_ID = 'test_client_id';
process.env.SPOTIFY_CLIENT_SECRET = 'test_client_secret';
process.env.AUTH_REDIRECT_URI = 'test_auth_redirect_uri';
process.env.CREATE_PLAYLIST_LAMBDA_ARN = 'test_lambda_arn';

const expectedBasicToken = generateBasicToken();
const expectedAuthCode = 'test_auth_code';
Expand Down
Loading