Skip to content
This repository has been archived by the owner on Oct 4, 2024. It is now read-only.

Integration with FitBit #185

Merged
merged 15 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ ENV NODE_ENV production

RUN npm run build

RUN npm run db:migrate

EXPOSE 3000

CMD [ "npm", "run", "start:prod" ]
7 changes: 7 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"verify": "npm run build && npm run check && npm run test && npm run test:e2e",
"build": "prisma generate && nest build",
"start": "nest start",
"start:dev": "npm run db:reset && nest start --watch",
"start:dev": "npm run db:migrate && nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
Expand All @@ -22,7 +22,8 @@
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"db:reset": "prisma migrate reset --force"
"db:reset": "prisma migrate reset --force",
"db:migrate": "prisma migrate deploy"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
Expand All @@ -36,6 +37,7 @@
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dayjs": "^1.11.11",
"nodemailer": "^6.9.13",
"passport": "^0.7.0",
"passport-http": "^0.3.0",
Expand Down Expand Up @@ -86,7 +88,10 @@
"coveragePathIgnorePatterns": [
"src/db/prisma.seed.ts",
"main.ts"
]
],
"moduleNameMapper": {
"src/(.*)": "<rootDir>/src/"
}
},
"prisma": {
"seed": "ts-node src/db/prisma.seed.ts"
Expand Down
13 changes: 0 additions & 13 deletions backend/prisma/migrations/20240402121953_init/migration.sql

This file was deleted.

30 changes: 30 additions & 0 deletions backend/prisma/migrations/20240519124445_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"displayName" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"verified" BOOLEAN NOT NULL DEFAULT false
);

-- CreateTable
CREATE TABLE "FitnessProviderCredential" (
"type" TEXT NOT NULL,
"refreshToken" TEXT NOT NULL,
"accessToken" TEXT NOT NULL,
"accessTokenExpires" DATETIME NOT NULL,
"userId" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL,
"providerUserId" TEXT NOT NULL,
CONSTRAINT "FitnessProviderCredential_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

-- CreateIndex
CREATE UNIQUE INDEX "FitnessProviderCredential_type_key" ON "FitnessProviderCredential"("type");

-- CreateIndex
CREATE UNIQUE INDEX "FitnessProviderCredential_userId_key" ON "FitnessProviderCredential"("userId");
22 changes: 17 additions & 5 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,23 @@ datasource db {
}

model User {
id String @id @default(uuid())
id String @id @default(uuid())
displayName String
email String @unique
email String @unique
password String
enabled Boolean @default(true)
verified Boolean @default(false)
notificationMethod String @default("EMAIL")
enabled Boolean @default(true)
verified Boolean @default(false)
providers FitnessProviderCredential[]
notificationMethod String @default("EMAIL")
}

model FitnessProviderCredential {
type String @unique
benedictweis marked this conversation as resolved.
Show resolved Hide resolved
refreshToken String
accessToken String
accessTokenExpires DateTime
owner User @relation(fields: [userId], references: [id])
userId String @unique
enabled Boolean
providerUserId String
}
135 changes: 135 additions & 0 deletions backend/src/api/datasource/datasource.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Test } from '@nestjs/testing';
import { PrismaModule } from '../../db/prisma.module';
import { FitnessService } from '../../integration/fitness/fitness.service';
import { DeepMockProxy, mockDeep } from 'jest-mock-extended';
import { MockProvider } from '../../integration/fitness/providers/mock.provider';
import { DatasourceController } from './datasource.controller';
import { NestRequest } from '../../types/request.type';
import { FitnessRepository } from '../../db/repositories/fitness.repository';
import { Response } from 'express';
import { TestConstants } from '../../../test/lib/constants';
import { ConsoleLogger } from '@nestjs/common';
import { LOGGER_SERVICE } from '../../logger/logger.service';

describe('Datasource controller', () => {
let fitnessService: DeepMockProxy<FitnessService>;
let fitnessController: DeepMockProxy<DatasourceController>;
let fitnessRepository: DeepMockProxy<FitnessRepository>;
let mockRequest: NestRequest;

beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [PrismaModule],
providers: [
FitnessService,
{ useClass: ConsoleLogger, provide: LOGGER_SERVICE },
],
controllers: [DatasourceController],
})
.overrideProvider(FitnessService)
.useValue(mockDeep<FitnessService>())
.overrideProvider(FitnessRepository)
.useValue(mockDeep<FitnessRepository>())
.compile();

fitnessService = module.get(FitnessService);
fitnessController = module.get(DatasourceController);
fitnessRepository = module.get(FitnessRepository);

mockRequest = {
user: {
id: 'MOCK',
},
} as NestRequest;
});

it('It should return the datasources for a user', async () => {
// Mock the datasources
fitnessService.getDatasourcesForUser.mockResolvedValue([
new MockProvider(),
]);

const userProviders =
await fitnessController.getDatasourcesForUser(mockRequest);

expect(userProviders.length).toBe(1);
});

it('Should return a specific datasource for the user', async () => {
fitnessService.getProviderForUserById.mockResolvedValue(new MockProvider());

const mockProvider = await fitnessController.getDatasourceForUser(
mockRequest,
{ id: 'MOCK' },
);

expect(mockProvider).toStrictEqual(new MockProvider().getInfo());
});

it('Should be able to delete a fitness provider', async () => {
fitnessRepository.getProviderForUserById.mockResolvedValue(
TestConstants.database.fitnessCredentials.fitbit,
);

const mockResponse = mockDeep<Response>();

mockResponse.status.mockReturnThis();

await fitnessController.deleteDatasource(
mockRequest,
{ id: 'MOCK' },
mockResponse,
);

expect(mockResponse.status).toHaveBeenCalledWith(204);
});

it('should resolve the authorize url of the provider', async () => {
fitnessService.getProviderForUserById.mockResolvedValue(new MockProvider());

const mockResponse = mockDeep<Response>();
mockResponse.status.mockReturnThis();

await fitnessController.getAuthorizeURL(
mockRequest,
{ id: 'MOCK' },
mockResponse,
);

expect(mockResponse.status).toHaveBeenCalledWith(200);
expect(mockResponse.json).toHaveBeenCalledWith({
url: 'MOCK_AURL',
});
});

it('should provide the access token by the user', async () => {
const mockedProvider = new MockProvider();
fitnessService.getProviderForUserById.mockResolvedValue(mockedProvider);

mockRequest.query = {
code: 'MOCKED_AP',
} as any;

const mockResponse = mockDeep<Response>();
mockResponse.status.mockReturnThis();

await fitnessController.redirect(mockRequest, 'MOCK', mockResponse);

expect(mockResponse.status).toHaveBeenCalledWith(200);
});

it('should return the daily goals', async () => {
fitnessService.getProviderForUserById.mockResolvedValue(new MockProvider());

mockRequest.query = {
code: 'MOCKED_AP',
} as any;

const mockResponse = mockDeep<Response>();
mockResponse.status.mockReturnThis();

await fitnessController.getGoals(mockRequest, 'MOCK', mockResponse);

expect(mockResponse.status).toHaveBeenCalledWith(200);
});
});
Loading
Loading