Skip to content

Commit

Permalink
Feature/permissions (#1176)
Browse files Browse the repository at this point in the history
Co-authored-by: Clément JANIN <clement.janin@cds-snc.ca>
  • Loading branch information
bryan-robitaille and craigzour committed Oct 31, 2022
1 parent 30e62a5 commit 496a662
Show file tree
Hide file tree
Showing 83 changed files with 6,519 additions and 3,285 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- Updated Terms and conditions page + text link in the footer [#863](https://github.com/cds-snc/platform-forms-client/issues/863)
- Modified Role Based to Asset Based Access Control [#1176](https://github.com/cds-snc/platform-forms-client/pull/1176)

### Fixed

Expand Down Expand Up @@ -129,7 +130,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Renamed `organisation` to `organization` which has an impact on the API access path
- Modified the middleware functionality and separation of scopes between middlewares
- A user now needs to have an enabled admin flag (user table) to access the Admin Pages
- An admin user can now add and remove administrative priveleges from other users.
- An admin user can now add and remove administrative privileges from other users.

### Fixed

Expand Down
30 changes: 25 additions & 5 deletions __tests__/api/acceptable-use.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,32 @@ import { createMocks } from "node-mocks-http";
import acceptableUse from "@pages/api/acceptableuse";
import { setAcceptableUse } from "@lib/acceptableUseCache";
import { getCsrfToken } from "next-auth/react";
import { unstable_getServerSession } from "next-auth/next";
import { Session } from "next-auth";

jest.mock("next-auth/next");
jest.mock("next-auth/react");
jest.mock("@lib/acceptableUseCache");
const mockedSetAcceptableUse = jest.mocked(setAcceptableUse, true);
const mockedGetCsrfToken = jest.mocked(getCsrfToken, true);
const mockedSetAcceptableUse = jest.mocked(setAcceptableUse, { shallow: true });
const mockedGetCsrfToken = jest.mocked(getCsrfToken, { shallow: true });
//Needed in the typescript version of the test so types are inferred correclty
const mockGetSession = jest.mocked(unstable_getServerSession, { shallow: true });

describe("Test acceptable use endpoint", () => {
beforeEach(() => {
const mockSession: Session = {
expires: "1",
user: {
id: "1",
email: "forms@cds.ca",
name: "forms user",
privileges: [],
},
};

mockGetSession.mockResolvedValue(mockSession);
});
afterEach(() => mockGetSession.mockReset());
mockedGetCsrfToken.mockResolvedValue("CsrfToken");
it("Should set acceptableuse value to true for userID 1 and return 200", async () => {
const { req, res } = createMocks({
Expand All @@ -30,7 +49,8 @@ describe("Test acceptable use endpoint", () => {
expect(res.statusCode).toBe(200);
});

it("Should return 404 for undefined userID", async () => {
it("Should return 401 for unauthenticated user", async () => {
mockGetSession.mockReset();
const { req, res } = createMocks({
method: "POST",
headers: {
Expand All @@ -43,8 +63,8 @@ describe("Test acceptable use endpoint", () => {
},
});
await acceptableUse(req, res);
expect(res.statusCode).toBe(404);
expect(JSON.parse(res._getData())).toEqual(expect.objectContaining({ error: "Bad request" }));
expect(res.statusCode).toBe(401);
expect(JSON.parse(res._getData())).toEqual(expect.objectContaining({ error: "Unauthorized" }));
});

it("Should throw an error and return 500 status code", async () => {
Expand Down
280 changes: 195 additions & 85 deletions __tests__/api/flags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,123 +7,233 @@ import enable from "@pages/api/flags/[key]/enable";
import disable from "@pages/api/flags/[key]/disable";
import check from "@pages/api/flags/[key]/check";
import checkAllFlags from "@pages/api/flags";
import defaultFlags from "../../flag_initialization/default_flag_settings.json";
import * as flags from "@lib/flags";
import { UserRole } from "@prisma/client";
jest.mock("next-auth/next");

//Needed in the typescript version of the test so types are inferred correclty
const mockGetSession = jest.mocked(unstable_getServerSession, true);
import { ViewApplicationSettings, ManageApplicationSettings } from "__utils__/permissions";
import Redis from "ioredis-mock";
const redis = new Redis();

jest.mock("@lib/flags");
jest.mock("@lib/integration/redisConnector", () => ({
getRedisInstance: jest.fn(() => redis),
}));

const mockedFlags = jest.mocked(flags, true);
jest.mock("next-auth/next");

const { checkOne, checkAll, enableFlag, disableFlag } = mockedFlags;
//Needed in the typescript version of the test so types are inferred correclty
const mockGetSession = jest.mocked(unstable_getServerSession, { shallow: true });

describe("Flags API endpoint", () => {
beforeAll(() => {
// Adding URL to process.env because we are mocking out Redis for these tests
process.env.REDIS_URL = "test_url";
});
afterAll(() => {
delete process.env.REDIS_URL;
});
describe("Authenticated", () => {
beforeEach(() => {
const mockSession = {
expires: "1",
user: {
email: "forms@cds.ca",
name: "forms user",
role: UserRole.ADMINISTRATOR,
userId: "1",
},
};
mockGetSession.mockResolvedValue(mockSession);
});
afterEach(() => {
mockGetSession.mockReset();
});

it("Enable a feature flag", async () => {
enableFlag.mockImplementation(jest.fn());
checkAll.mockResolvedValue(defaultFlags);

const { req, res } = createMocks({
method: "GET",
headers: {
"Content-Type": "application/json",
Origin: "http://localhost:3000",
},
url: "/api/flags/featureFlag/enable",
query: {
key: "featureFlag",
},
describe("ViewApplicationSettings", () => {
beforeEach(async () => {
const mockSession = {
expires: "1",
user: {
email: "forms@cds.ca",
name: "forms user",
id: "1",
privileges: ViewApplicationSettings,
},
};
mockGetSession.mockResolvedValue(mockSession);
// Intialize mockRedis with default flags
const testFlags = {
flag1: true,
flag2: false,
};
for (const [key, value] of Object.entries(testFlags)) {
await redis
.multi()
.sadd("flags", key)
.set(`flag:${key}`, value ? "1" : "0")
.exec();
}
});
afterEach(() => {
redis.flushall();
mockGetSession.mockReset();
});

await enable(req, res);
it("Enable a feature flag", async () => {
const { req, res } = createMocks({
method: "GET",
headers: {
"Content-Type": "application/json",
Origin: "http://localhost:3000",
},
url: "/api/flags/featureFlag/enable",
query: {
key: "featureFlag",
},
});

await enable(req, res);

expect(res.statusCode).toBe(403);
expect(res._getJSONData()).toMatchObject({ error: "Forbidden" });
});

expect(res.statusCode).toBe(200);
expect(res._getJSONData()).toMatchObject(defaultFlags);
expect(enableFlag).toHaveBeenCalledWith("featureFlag");
it("Disable a feature flag", async () => {
const { req, res } = createMocks({
method: "GET",
headers: {
"Content-Type": "application/json",
Origin: "http://localhost:3000",
},
url: "/api/flags/featureFlag/disable",
query: {
key: "featureFlag",
},
});

await disable(req, res);

expect(res.statusCode).toBe(403);
expect(res._getJSONData()).toMatchObject({ error: "Forbidden" });
});
it("Gets a list of all feature flags", async () => {
const { req, res } = createMocks({
method: "GET",
headers: {
"Content-Type": "application/json",
Origin: "http://localhost:3000",
},
url: "/api/flags/",
});

await checkAllFlags(req, res);

expect(res.statusCode).toBe(200);
expect(res._getJSONData()).toMatchObject({ flag1: true, flag2: false });
});
});

it("Disable a feature flag", async () => {
disableFlag.mockImplementation(jest.fn());
checkAll.mockResolvedValue(defaultFlags);

const { req, res } = createMocks({
method: "GET",
headers: {
"Content-Type": "application/json",
Origin: "http://localhost:3000",
},
url: "/api/flags/featureFlag/disable",
query: {
key: "featureFlag",
},
describe("ManageApplicationSettings", () => {
beforeEach(async () => {
const mockSession = {
expires: "1",
user: {
email: "forms@cds.ca",
name: "forms user",
id: "1",
privileges: ManageApplicationSettings,
},
};
mockGetSession.mockResolvedValue(mockSession);

// Intialize mockRedis with default flags
const testFlags = {
flag1: true,
flag2: false,
};
for (const [key, value] of Object.entries(testFlags)) {
await redis
.multi()
.sadd("flags", key)
.set(`flag:${key}`, value ? "1" : "0")
.exec();
}
});
afterEach(() => {
redis.flushall();
mockGetSession.mockReset();
});

await disable(req, res);
it("Enable a feature flag", async () => {
const { req, res } = createMocks({
method: "GET",
headers: {
"Content-Type": "application/json",
Origin: "http://localhost:3000",
},
url: "/api/flags/flag2/enable",
query: {
key: "flag2",
},
});

await enable(req, res);

expect(res.statusCode).toBe(200);
expect(res._getJSONData()).toMatchObject({ flag2: true });
});

expect(res.statusCode).toBe(200);
expect(res._getJSONData()).toMatchObject(defaultFlags);
expect(disableFlag).toHaveBeenCalledWith("featureFlag");
it("Disable a feature flag", async () => {
const { req, res } = createMocks({
method: "GET",
headers: {
"Content-Type": "application/json",
Origin: "http://localhost:3000",
},
url: "/api/flags/flag1/disable",
query: {
key: "flag1",
},
});

await disable(req, res);

expect(res.statusCode).toBe(200);
expect(res._getJSONData()).toMatchObject({ flag1: false });
});
it("Gets a list of all feature flags", async () => {
const { req, res } = createMocks({
method: "GET",
headers: {
"Content-Type": "application/json",
Origin: "http://localhost:3000",
},
url: "/api/flags/",
});

await checkAllFlags(req, res);

expect(res.statusCode).toBe(200);
expect(res._getJSONData()).toMatchObject({ flag1: true, flag2: false });
});
});
});

describe("Unauthenticated", () => {
it("Check a feature flag", async () => {
(checkOne as jest.Mock).mockReturnValue(true);
beforeAll(async () => {
// Intialize mockRedis with default flags
const testFlags = {
flag1: true,
flag2: false,
};
for (const [key, value] of Object.entries(testFlags)) {
await redis
.multi()
.sadd("flags", key)
.set(`flag:${key}`, value ? "1" : "0")
.exec();
}
});
afterAll(() => {
redis.flushall();
});

it("Check a feature flag", async () => {
const { req, res } = createMocks({
method: "GET",
headers: {
"Content-Type": "application/json",
Origin: "http://localhost:3000",
},
url: "/api/flags/featureFlag/check",
url: "/api/flags/flag1/check",
query: {
key: "featureFlag",
key: "flag1",
},
});

await check(req, res);

expect(res.statusCode).toBe(200);
expect(res._getJSONData()).toMatchObject({ status: true });
expect(checkOne).toHaveBeenCalledWith("featureFlag");
});
it("Gets a list of all feature flags", async () => {
checkAll.mockResolvedValue(defaultFlags);

const { req, res } = createMocks({
method: "GET",
headers: {
"Content-Type": "application/json",
Origin: "http://localhost:3000",
},
url: "/api/flags/",
});

await checkAllFlags(req, res);

expect(res.statusCode).toBe(200);
expect(res._getJSONData()).toMatchObject(defaultFlags);
});
});
});

0 comments on commit 496a662

Please sign in to comment.